Apéndice 4.2 Google Colab¶
Algoritmos de Mejoramiento de Imágenes usando Operaciones Morfológicas¶
Maestría en Inteligencia Artificial Aplicada¶
Tecnológico de Monterrey
Profesores¶
- Profesor Titular: Dr. Gilberto Ochoa Ruiz
- Profesora Asistente: MIP Ma. del Refugio Melendez Alfaro
- Profesor Tutor: M. en C. Jose Angel Martinez Navarro
Team 13 - Integrantes¶
| Nombre | Matrícula |
|---|---|
| Javier Augusto Rebull Saucedo | A01795838 |
| Juan Carlos Pérez Nava | A01795941 |
| Luis Gerardo Sánchez Salazar | A01232963 |
| Oscar Enrique García García | A01016093 |
Información del Proyecto¶
- Curso: Visión Computacional para Imágenes y Video
- Actividad: Mini Proyecto - Detección de Placas Vehiculares
- Fecha de Entrega: Octubre 5, 2025
- Modalidad: Equipo
Descripción¶
Este notebook presenta un apéndice práctico de la Actividad 4.2, donde se implementa un pipeline completo de detección automática de placas vehiculares utilizando todas las operaciones morfológicas estudiadas en el curso: erosión, dilatación, apertura, cierre, gradiente morfológico y top-hat.
Apéndice: Aplicación Práctica de Operaciones Morfológicas¶
Mini Proyecto - Detección y Extracción de Placas Vehiculares¶
Descripción¶
Este apéndice presenta una aplicación práctica integral de las operaciones morfológicas estudiadas en la Actividad 4.2, implementando un pipeline completo de detección automática de placas vehiculares que utiliza todas las técnicas morfológicas del curso principal.
Estructura del Notebook¶
CELDA 1: Instalación de Librerías e Imports¶
Propósito: Configuración inicial del entorno de trabajo
- Instala las librerías necesarias (OpenCV, rawpy, pillow-heif, gdown)
- Importa todas las dependencias para procesamiento de imágenes
- Verifica la ubicación geográfica y timestamp de ejecución
- Inicializa el cronómetro del notebook
Operaciones Morfológicas: Ninguna (configuración)
CELDA 2: Descarga de Imágenes desde Google Drive¶
Propósito: Obtención del dataset de placas vehiculares
- Define el inventario de imágenes con IDs de Google Drive
- Genera mapeo secuencial de nombres (Plate 00, Plate 01, etc.)
- Descarga automáticamente todas las imágenes
- Crea archivo CSV con el mapping de nombres originales a nuevos
Operaciones Morfológicas: Ninguna (adquisición de datos)
CELDA 3: Procesamiento y Redimensión de Imágenes¶
Propósito: Preparación inicial del dataset
- Lee archivos en múltiples formatos (JPG, HEIC, DNG, RAW)
- Redimensiona imágenes a ancho máximo de 800px
- Genera versiones en BGR, RGB, escala de grises y binarias
- Almacena todas las versiones en listas separadas
Operaciones Morfológicas:
- Binarización con umbral de Otsu (preparación para morfología)
CELDA 4: Visualización de Imágenes Procesadas¶
Propósito: Inspección visual del preprocesamiento
- Muestra comparación de formatos: BGR, RGB y Escala de Grises
- Valida la calidad de la conversión de formatos
- Permite identificar problemas en la carga de imágenes
Operaciones Morfológicas: Ninguna (visualización)
CELDA 5: Dilatación Inicial (Batch)¶
Propósito: Primera operación morfológica sobre todas las placas
- Convierte todas las imágenes a escala de grises
- Aplica binarización con umbral 127
- Dilatación: Kernel 3×3, 1 iteración sobre imágenes grises y binarias
- Almacena resultados en listas para procesamiento posterior
Operaciones Morfológicas:
- ✅ Dilatación (expanding de regiones blancas)
CELDA 6: Visualización Comparativa de Placas¶
Propósito: Comparación de efectos morfológicos iniciales
- Muestra 5 versiones de cada placa: Original (color), Grises, Dilatación Grises, Binaria, Dilatación Binaria
- Permite evaluar el impacto de la dilatación inicial
- Grid visual con todas las placas procesadas
Operaciones Morfológicas: Ninguna (visualización)
CELDA 7: Análisis con Histogramas¶
Propósito: Análisis cuantitativo del procesamiento
- Visualiza diferencias antes/después de dilatación
- Genera histogramas comparativos de distribución de píxeles
- Calcula diferencias binarias para visualizar píxeles añadidos
Operaciones Morfológicas: Ninguna (análisis)
CELDA 8: Gradiente Morfológico + Umbralización¶
Propósito: Detección de bordes mediante morfología
- Filtro bilateral para suavizado preservando bordes
- Gradiente Morfológico: Dilatación - Erosión (kernel 3×3)
- Umbralización de Otsu sobre el gradiente
- Cierre morfológico: Kernel 30×5, 3 iteraciones (conectar caracteres)
- Filtrado de contornos por área mínima
Operaciones Morfológicas:
- ✅ Dilatación (parte del gradiente)
- ✅ Erosión (parte del gradiente)
- ✅ Gradiente Morfológico (Dilation - Erosion)
- ✅ Cierre (Closing) (unir regiones fragmentadas)
CELDA 9: Detección Inteligente de Región de Placa¶
Propósito: Pre-localización de la zona de la placa
- Ecualización adaptativa CLAHE
- Umbralización de Otsu para detectar regiones claras (placas típicamente son claras)
- Cierre morfológico: Kernel 15×5 (conectar caracteres)
- Apertura morfológica: Kernel 3×3 (eliminar ruido)
- Filtrado por aspect ratio (1.5-6.0) y área (>5000)
- Detección de Canny solo en región de interés
Operaciones Morfológicas:
- ✅ Cierre (Closing) (conectar caracteres de la placa)
- ✅ Apertura (Opening) (eliminar ruido pequeño)
CELDA 10: Pipeline Morfológico Completo¶
Propósito: Implementación del pipeline definitivo de detección
- Filtro bilateral (preservar bordes)
- Apertura: Kernel 3×3 (limpieza inicial de ruido)
- Top-Hat: Kernel 35×18 (extraer placas claras del fondo oscuro)
- Gradiente Morfológico: Kernel 3×3 (resaltar bordes)
- Combinación ponderada: 70% Top-Hat + 30% Gradiente
- Binarización de Otsu
- Cierre agresivo: Kernel 30×5, 3 iteraciones (unir caracteres)
- Apertura: Kernel 5×5 (limpieza final)
- Dilatación: Kernel 20×8, 2 iteraciones (expandir región final)
- Filtrado de contornos por área >2000
Operaciones Morfológicas:
- ✅ Apertura (Opening) (limpieza de ruido)
- ✅ Top-Hat (extraer objetos brillantes)
- ✅ Gradiente Morfológico (detección de bordes)
- ✅ Cierre (Closing) (conectar caracteres)
- ✅ Apertura (Opening) (limpieza final)
- ✅ Dilatación (expansión de región)
- ✅ Erosión (implícita en operaciones compuestas)
CELDA 11: Filtrado por Aspect Ratio¶
Propósito: Selección del mejor candidato por características geométricas
- Búsqueda de contornos en máscaras morfológicas finales
- Filtrado por:
- Área mínima: 18,000 px
- Aspect Ratio: 1.9 - 4.0 (placas horizontales)
- Solidez mínima: 0.25
- Sistema de scoring para seleccionar mejor candidato
- Visualización con bounding boxes
Operaciones Morfológicas: Ninguna (post-procesamiento geométrico)
CELDA 12: Recorte de Placas Detectadas¶
Propósito: Extracción de ROI (Región de Interés)
- Recorte de placas usando bounding boxes de mejor candidato
- Margen de 10 píxeles para capturar contexto
- Generación de versiones en color y escala de grises
- Almacenamiento para fase de enderezado
Operaciones Morfológicas: Ninguna (extracción de ROI)
CELDA 13: Detección y Extracción (Versión Mejorada)¶
Propósito: Pipeline alternativo usando máscaras de Celda 10
- Utiliza directamente
todas_dilataciones_binariasde Celda 10 - Parámetros adaptativos: AR 1.5-8.0, Área >15,000
- Scoring basado en área, aspect ratio, solidez y extent
- Visualización de 3 imágenes: Original, Máscara Final, Placa Extraída
- Compatible con Celda 14 (Rotación)
Operaciones Morfológicas: Ninguna (usa resultados de Celda 10)
CELDA 14: Corrección de Rotación (Batch)¶
Propósito: Normalización de orientación de placas
- Estrategia simple: detectar si width < height
- Rotación automática de 90° para placas verticales
- Preservación de placas ya horizontales
- Visualización comparativa antes/después
- Preparación para OCR o análisis posterior
Operaciones Morfológicas: Ninguna (transformación geométrica)
Resumen de Operaciones Morfológicas Utilizadas¶
| Operación | Celdas | Aplicación en el Pipeline |
|---|---|---|
| Dilatación | 5, 8, 10 | Expansión de regiones, parte del gradiente, expansión final |
| Erosión | 8, 10 | Parte del gradiente, refinamiento |
| Apertura (Opening) | 9, 10 | Eliminación de ruido pequeño |
| Cierre (Closing) | 8, 9, 10 | Conexión de caracteres fragmentados |
| Gradiente Morfológico | 8, 10 | Detección de bordes y transiciones |
| Top-Hat | 10 | Extracción de placas claras del fondo oscuro |
Justificación Técnica¶
Este proyecto demuestra cómo las operaciones morfológicas no son técnicas aisladas, sino herramientas que se combinan estratégicamente. El pipeline utiliza:
- Dilatación inicial (Celda 5): Exploración de conectividad
- Gradiente (Celda 8): Primera detección de bordes
- Opening/Closing (Celda 9): Limpieza y conexión inteligente
- Top-Hat (Celda 10): Extracción específica de objetos brillantes
- Pipeline completo (Celda 10): Combinación estratégica de todas las operaciones
Cada operación tiene un propósito específico que contribuye al objetivo final: detección robusta de placas vehiculares en condiciones variables de iluminación y calidad.
Nota: Este apéndice complementa la Actividad 4.2 y demuestra la aplicación secuencial de todas las operaciones morfológicas estudiadas en un caso de uso real de producción.
# CELDA 1:
# ===============================================================
# INSTALACIÓN DE LIBRERÍAS
# ===============================================================
!pip install -q pillow-heif opencv-python-headless
!pip install -q rawpy gdown
# ===============================================================
# MANEJO DE DATOS Y COMPUTACIÓN CIENTÍFICA
# ===============================================================
import numpy as np # Arrays y operaciones matemáticas
# ===============================================================
# PROCESAMIENTO DE IMÁGENES
# ===============================================================
import cv2 # OpenCV para manipulación y filtros
from PIL import Image # Manipulación de imágenes
import pillow_heif # Formato HEIC
import rawpy # Archivos RAW
# ===============================================================
# VISUALIZACIÓN DE DATOS
# ===============================================================
import matplotlib.pyplot as plt # Gráficos y visualizaciones
# ===============================================================
# UTILIDADES Y MANEJO DEL SISTEMA
# ===============================================================
import os # Operaciones del sistema
from pathlib import Path # Manejo de rutas
import shutil # Operaciones con archivos
from csv import DictWriter # Escritura de CSV
from urllib.request import urlopen # Solicitudes HTTP
from datetime import datetime # Manejo de fechas
from zoneinfo import ZoneInfo # Zonas horarias
import time # Tiempo de ejecución
# ===============================================================
# GOOGLE DRIVE
# ===============================================================
try:
import gdown # Descarga desde Google Drive
except ImportError as e:
raise SystemExit(
"No se encontró 'gdown'. Instálalo con:\n\n pip install gdown\n"
)
# ===============================================================
# MENSAJE DE CONFIRMACIÓN
# ===============================================================
print("Librerías cargadas y listas para la Visión")
print(f"Timestamp de ejecución: {datetime.now(ZoneInfo('America/Mexico_City')).strftime('%Y-%m-%d %H:%M:%S')}")
try:
with urlopen('https://ipapi.co/country_name/') as response:
country = response.read().decode('utf-8').strip()
print(f"País de ejecución: {country}")
with urlopen('https://ipapi.co/region/') as response:
region = response.read().decode('utf-8').strip()
print(f"Estado/Región de ejecución: {region}")
except Exception as e:
print("No se pudo determinar el país o estado de ejecución (posiblemente sin conexión a internet).")
notebook_start_time = time.time()
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 5.5/5.5 MB 44.8 MB/s eta 0:00:00 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.9/1.9 MB 20.0 MB/s eta 0:00:00 Librerías cargadas y listas para la Visión Timestamp de ejecución: 2025-10-05 10:01:27 País de ejecución: United States Estado/Región de ejecución: South Carolina
# CELDA 2:
# ===============================================================
# 1) INVENTARIO FUENTE: (nombre_original, drive_id)
# ===============================================================
fuente = [
# De tu lista 00–11
("Plate 00.JPG", "17J6nKFJjKZWWXVqeoNonfBZ4gkF0N5O4"),
("Plate 01.HEIC", "1o_QIWc1TW55r26u_PWRFoeysvIIlGFMH"),
("Plate 02.JPG", "13CTNshx-qQuzacpDBJoYSI9cxMD58dZD"),
("Plate 03.HEIC", "1U_nWjWBJHG44rdaQ2cOGRvYZTaxdMmSx"),
("Plate 04.HEIC", "16kRnLY_uNZrc4JCK2D68hZ7WYE6QF9p9"),
("Plate 05.HEIC", "1jXp0VhxKIczCkdySnFxBMr2Cm9ekI14b"),
("Plate 06.HEIC", "1ib4v9YAE85LWhQ0CXGHbApc7fvCWccBL"),
("Plate 07.HEIC", "1b7SvhrbfCEsd5Iu393oe81hFI8NpNilN"),
("Plate 08.HEIC", "1NzoSVcbmPk_eH54GRmWneETb12EhtoCF"),
("Plate 09.HEIC", "1Ku7m5t6BDixMvCxbT5WA_1-5zgYM0hK0"),
("Plate 10.heic", "19W2zCiE5syb0kzzmk3kPIy7ibDjbt_m_"),
("Plate 11.HEIC", "1p_xtpHrKZ_c7N0xH-fa5bn01Bf4z9817"),
("p2b. Rebull Plate.DNG", "1QsnlZcK-2YE35o3AMiX81xH-LgITWGsh"),
("pb2. MCL878 Florida Plate.DNG","12QMq8QqzpAQCJxNrqObcjQGiSOiugBnq"),
("pb2. Maine Plate.DNG", "1hHNc_sb93jDYtUHct5YakVq8Rcf_U3uA"),
("pb2. Veteran Plate.DNG", "19jGR_kfcyCS824HUYtdE2rdTvSiDHr4G"),
("p2b. MA Night 01.HEIC", "1jHCFZs7qV67WbHXyVQt_YBr_-jcoKmqi"),
("p2b. MA Night 02.HEIC", "1hnh6hPdQ-vdmAiWBGF-bmrEQ6Jo6Cgfb"),
("p2b. MA Night 03.HEIC", "1uaTxrYpgUiSUsJ7SuL85ytffJ767uPGL"),
]
# ===============================================================
# 2) FUNCIÓN: generar nombres "Plate XX" secuenciales (conserva extensión)
# ===============================================================
def generar_mapeo_secuencial(items, prefijo="Plate "):
"""
items: lista de tuplas (nombre_original, drive_id)
return:
- imagenes_plates: dict {nombre_nuevo: drive_id}
- renombres: dict {nombre_original: nombre_nuevo}
- filas_csv: lista de dicts para exportar mapping
"""
imagenes_plates = {}
renombres = {}
filas_csv = []
for i, (nombre, file_id) in enumerate(items):
ext = Path(nombre).suffix
ext = ext.upper() if ext else ""
nombre_nuevo = f"{prefijo}{i:02d}{ext}"
imagenes_plates[nombre_nuevo] = file_id
renombres[nombre] = nombre_nuevo
filas_csv.append({
"old_name": nombre,
"new_name": nombre_nuevo,
"drive_id": file_id,
})
return imagenes_plates, renombres, filas_csv
# ===============================================================
# 3) CONSTRUIR LOS DICCIONARIOS (secuenciales)
# ===============================================================
imagenes_plates, renombres, filas_csv = generar_mapeo_secuencial(fuente)
# ===============================================================
# 4) LIMPIEZA Y PREPARACIÓN
# ===============================================================
print("\n🧹 Limpiando directorio...")
directorio = "p2b_plates"
if os.path.exists(directorio):
shutil.rmtree(directorio)
print(f"✓ Carpeta '{directorio}' eliminada")
os.makedirs(directorio, exist_ok=True)
print(f"✓ Carpeta '{directorio}' creada\n")
# Guardamos el mapping como CSV para referencia
mapping_csv = os.path.join(directorio, "mapping.csv")
with open(mapping_csv, "w", newline="", encoding="utf-8") as f:
writer = DictWriter(f, fieldnames=["old_name", "new_name", "drive_id"])
writer.writeheader()
writer.writerows(filas_csv)
print(f"🗂️ Mapping guardado en: {mapping_csv}\n")
# ===============================================================
# 5) DESCARGA DE IMÁGENES (usa nombres NUEVOS secuenciales)
# ===============================================================
print("📥 Descargando imágenes de placas...\n")
for nombre_nuevo, file_id in imagenes_plates.items():
output_path = os.path.join(directorio, nombre_nuevo)
print(f"⬇️ Descargando '{nombre_nuevo}' (id={file_id})...")
# gdown permite usar id=... directamente
gdown.download(id=file_id, output=output_path, quiet=False)
print("\n✅ Descarga completada")
print("Resumen:")
print(f" - Total de elementos: {len(imagenes_plates)}")
print(f" - Directorio de salida: {os.path.abspath(directorio)}")
🧹 Limpiando directorio... ✓ Carpeta 'p2b_plates' creada 🗂️ Mapping guardado en: p2b_plates/mapping.csv 📥 Descargando imágenes de placas... ⬇️ Descargando 'Plate 00.JPG' (id=17J6nKFJjKZWWXVqeoNonfBZ4gkF0N5O4)...
Downloading... From: https://drive.google.com/uc?id=17J6nKFJjKZWWXVqeoNonfBZ4gkF0N5O4 To: /content/p2b_plates/Plate 00.JPG 100%|██████████| 1.19M/1.19M [00:00<00:00, 90.7MB/s]
⬇️ Descargando 'Plate 01.HEIC' (id=1o_QIWc1TW55r26u_PWRFoeysvIIlGFMH)...
Downloading... From: https://drive.google.com/uc?id=1o_QIWc1TW55r26u_PWRFoeysvIIlGFMH To: /content/p2b_plates/Plate 01.HEIC 100%|██████████| 2.93M/2.93M [00:00<00:00, 87.7MB/s]
⬇️ Descargando 'Plate 02.JPG' (id=13CTNshx-qQuzacpDBJoYSI9cxMD58dZD)...
Downloading... From: https://drive.google.com/uc?id=13CTNshx-qQuzacpDBJoYSI9cxMD58dZD To: /content/p2b_plates/Plate 02.JPG 100%|██████████| 450k/450k [00:00<00:00, 36.6MB/s]
⬇️ Descargando 'Plate 03.HEIC' (id=1U_nWjWBJHG44rdaQ2cOGRvYZTaxdMmSx)...
Downloading... From: https://drive.google.com/uc?id=1U_nWjWBJHG44rdaQ2cOGRvYZTaxdMmSx To: /content/p2b_plates/Plate 03.HEIC 100%|██████████| 2.23M/2.23M [00:00<00:00, 34.5MB/s]
⬇️ Descargando 'Plate 04.HEIC' (id=16kRnLY_uNZrc4JCK2D68hZ7WYE6QF9p9)...
Downloading... From: https://drive.google.com/uc?id=16kRnLY_uNZrc4JCK2D68hZ7WYE6QF9p9 To: /content/p2b_plates/Plate 04.HEIC 100%|██████████| 2.23M/2.23M [00:00<00:00, 23.1MB/s]
⬇️ Descargando 'Plate 05.HEIC' (id=1jXp0VhxKIczCkdySnFxBMr2Cm9ekI14b)...
Downloading... From: https://drive.google.com/uc?id=1jXp0VhxKIczCkdySnFxBMr2Cm9ekI14b To: /content/p2b_plates/Plate 05.HEIC 100%|██████████| 3.96M/3.96M [00:00<00:00, 200MB/s]
⬇️ Descargando 'Plate 06.HEIC' (id=1ib4v9YAE85LWhQ0CXGHbApc7fvCWccBL)...
Downloading... From: https://drive.google.com/uc?id=1ib4v9YAE85LWhQ0CXGHbApc7fvCWccBL To: /content/p2b_plates/Plate 06.HEIC 100%|██████████| 3.96M/3.96M [00:00<00:00, 137MB/s]
⬇️ Descargando 'Plate 07.HEIC' (id=1b7SvhrbfCEsd5Iu393oe81hFI8NpNilN)...
Downloading... From: https://drive.google.com/uc?id=1b7SvhrbfCEsd5Iu393oe81hFI8NpNilN To: /content/p2b_plates/Plate 07.HEIC 100%|██████████| 2.16M/2.16M [00:00<00:00, 127MB/s]
⬇️ Descargando 'Plate 08.HEIC' (id=1NzoSVcbmPk_eH54GRmWneETb12EhtoCF)...
Downloading... From: https://drive.google.com/uc?id=1NzoSVcbmPk_eH54GRmWneETb12EhtoCF To: /content/p2b_plates/Plate 08.HEIC 100%|██████████| 1.86M/1.86M [00:00<00:00, 157MB/s]
⬇️ Descargando 'Plate 09.HEIC' (id=1Ku7m5t6BDixMvCxbT5WA_1-5zgYM0hK0)...
Downloading... From: https://drive.google.com/uc?id=1Ku7m5t6BDixMvCxbT5WA_1-5zgYM0hK0 To: /content/p2b_plates/Plate 09.HEIC 100%|██████████| 1.86M/1.86M [00:00<00:00, 76.9MB/s]
⬇️ Descargando 'Plate 10.HEIC' (id=19W2zCiE5syb0kzzmk3kPIy7ibDjbt_m_)...
Downloading... From: https://drive.google.com/uc?id=19W2zCiE5syb0kzzmk3kPIy7ibDjbt_m_ To: /content/p2b_plates/Plate 10.HEIC 100%|██████████| 2.27M/2.27M [00:00<00:00, 129MB/s]
⬇️ Descargando 'Plate 11.HEIC' (id=1p_xtpHrKZ_c7N0xH-fa5bn01Bf4z9817)...
Downloading... From: https://drive.google.com/uc?id=1p_xtpHrKZ_c7N0xH-fa5bn01Bf4z9817 To: /content/p2b_plates/Plate 11.HEIC 100%|██████████| 2.64M/2.64M [00:00<00:00, 177MB/s]
⬇️ Descargando 'Plate 12.DNG' (id=1QsnlZcK-2YE35o3AMiX81xH-LgITWGsh)...
Downloading... From: https://drive.google.com/uc?id=1QsnlZcK-2YE35o3AMiX81xH-LgITWGsh To: /content/p2b_plates/Plate 12.DNG 100%|██████████| 82.6M/82.6M [00:00<00:00, 137MB/s]
⬇️ Descargando 'Plate 13.DNG' (id=12QMq8QqzpAQCJxNrqObcjQGiSOiugBnq)...
Downloading... From: https://drive.google.com/uc?id=12QMq8QqzpAQCJxNrqObcjQGiSOiugBnq To: /content/p2b_plates/Plate 13.DNG 100%|██████████| 69.3M/69.3M [00:00<00:00, 146MB/s]
⬇️ Descargando 'Plate 14.DNG' (id=1hHNc_sb93jDYtUHct5YakVq8Rcf_U3uA)...
Downloading... From: https://drive.google.com/uc?id=1hHNc_sb93jDYtUHct5YakVq8Rcf_U3uA To: /content/p2b_plates/Plate 14.DNG 100%|██████████| 66.5M/66.5M [00:00<00:00, 118MB/s]
⬇️ Descargando 'Plate 15.DNG' (id=19jGR_kfcyCS824HUYtdE2rdTvSiDHr4G)...
Downloading... From: https://drive.google.com/uc?id=19jGR_kfcyCS824HUYtdE2rdTvSiDHr4G To: /content/p2b_plates/Plate 15.DNG 100%|██████████| 86.7M/86.7M [00:00<00:00, 152MB/s]
⬇️ Descargando 'Plate 16.HEIC' (id=1jHCFZs7qV67WbHXyVQt_YBr_-jcoKmqi)...
Downloading... From: https://drive.google.com/uc?id=1jHCFZs7qV67WbHXyVQt_YBr_-jcoKmqi To: /content/p2b_plates/Plate 16.HEIC 100%|██████████| 3.05M/3.05M [00:00<00:00, 93.5MB/s]
⬇️ Descargando 'Plate 17.HEIC' (id=1hnh6hPdQ-vdmAiWBGF-bmrEQ6Jo6Cgfb)...
Downloading... From: https://drive.google.com/uc?id=1hnh6hPdQ-vdmAiWBGF-bmrEQ6Jo6Cgfb To: /content/p2b_plates/Plate 17.HEIC 100%|██████████| 3.22M/3.22M [00:00<00:00, 149MB/s]
⬇️ Descargando 'Plate 18.HEIC' (id=1uaTxrYpgUiSUsJ7SuL85ytffJ767uPGL)...
Downloading... From: https://drive.google.com/uc?id=1uaTxrYpgUiSUsJ7SuL85ytffJ767uPGL To: /content/p2b_plates/Plate 18.HEIC 100%|██████████| 2.76M/2.76M [00:00<00:00, 55.8MB/s]
✅ Descarga completada Resumen: - Total de elementos: 19 - Directorio de salida: /content/p2b_plates
# CELDA 3:
# ===================================================================
# FUNCIÓN AUXILIAR PARA REDIMENSIONAR IMÁGENES
# ===================================================================
def resize_image(image, max_width=800):
"""
Redimensiona una imagen manteniendo su relación de aspecto.
"""
height, width = image.shape[:2]
if width > max_width:
ratio = max_width / width
new_dimensions = (max_width, int(height * ratio))
return cv2.resize(image, new_dimensions, interpolation=cv2.INTER_AREA)
return image
# ===================================================================
# PROCESAMIENTO Y REDIMENSION DE IMÁGENES (p2b_plates)
# ===================================================================
# --- Listas NUEVAS solo para las imágenes de matrículas (plates) ---
imagenes_originales_p2b = []
imagenes_rgb_p2b = []
imagenes_grises_p2b = []
imagenes_binarias_p2b = []
imagenes_binarias_invertidas_p2b = []
nombres_archivos_procesados_p2b = []
# --- Directorio a procesar ---
directorio = "p2b_plates"
print(f"\n📂 Procesando todas las imágenes del directorio: '{directorio}'...")
try:
archivos = [f for f in os.listdir(directorio) if f.lower().endswith(('.jpg', '.jpeg', '.png', '.dng', '.heic', '.heif'))]
for nombre_archivo in archivos:
ruta_completa = os.path.join(directorio, nombre_archivo)
img = None
if nombre_archivo.lower().endswith('.dng'):
print(f" - Leyendo archivo RAW: {nombre_archivo}")
with rawpy.imread(ruta_completa) as raw:
rgb_array = raw.postprocess()
img = cv2.cvtColor(rgb_array, cv2.COLOR_RGB2BGR)
elif nombre_archivo.lower().endswith(('.heic', '.heif')):
print(f" - Leyendo archivo HEIC: {nombre_archivo}")
heif_file = pillow_heif.read_heif(ruta_completa)
image_pil = Image.frombytes(
heif_file.mode, heif_file.size, heif_file.data, "raw",
)
img = cv2.cvtColor(np.array(image_pil), cv2.COLOR_RGB2BGR)
else:
print(f" - Leyendo archivo: {nombre_archivo}")
img = cv2.imread(ruta_completa)
if img is not None:
img = resize_image(img, max_width=800)
rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
binr = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1]
invert = cv2.bitwise_not(binr)
# Guardar en las listas de matrículas (plates)
imagenes_originales_p2b.append(img)
imagenes_rgb_p2b.append(rgb)
imagenes_grises_p2b.append(gray)
imagenes_binarias_p2b.append(binr)
imagenes_binarias_invertidas_p2b.append(invert)
nombres_archivos_procesados_p2b.append(nombre_archivo)
else:
print(f"⚠️ No se pudo leer el archivo: {nombre_archivo}")
except FileNotFoundError:
print(f"❌ Error: El directorio '{directorio}' no fue encontrado.")
except Exception as e:
print(f"❌ Ocurrió un error inesperado al procesar el directorio '{directorio}': {e}")
# --- Mensaje final ---
print("\n✅ ¡Procesamiento completado!")
print(f" - Se cargaron {len(imagenes_originales_p2b)} imágenes en las listas de matrículas (plates).")
📂 Procesando todas las imágenes del directorio: 'p2b_plates'... - Leyendo archivo HEIC: Plate 01.HEIC - Leyendo archivo HEIC: Plate 17.HEIC - Leyendo archivo: Plate 00.JPG - Leyendo archivo HEIC: Plate 07.HEIC - Leyendo archivo: Plate 02.JPG - Leyendo archivo HEIC: Plate 18.HEIC - Leyendo archivo HEIC: Plate 04.HEIC - Leyendo archivo HEIC: Plate 06.HEIC - Leyendo archivo HEIC: Plate 10.HEIC - Leyendo archivo RAW: Plate 13.DNG - Leyendo archivo RAW: Plate 12.DNG - Leyendo archivo HEIC: Plate 11.HEIC - Leyendo archivo HEIC: Plate 09.HEIC - Leyendo archivo HEIC: Plate 16.HEIC - Leyendo archivo HEIC: Plate 03.HEIC - Leyendo archivo RAW: Plate 14.DNG - Leyendo archivo HEIC: Plate 08.HEIC - Leyendo archivo HEIC: Plate 05.HEIC - Leyendo archivo RAW: Plate 15.DNG ✅ ¡Procesamiento completado! - Se cargaron 19 imágenes en las listas de matrículas (plates).
# CELDA 4:
# --- Función adaptada para visualizar cualquier set de imágenes ---
def mostrar_comparacion_p2(img_bgr, img_rgb, img_gris, titulo_principal=""):
"""
Muestra tres versiones de una imagen: BGR, RGB y Escala de Grises.
"""
fig = plt.figure(figsize=(15, 6))
fig.suptitle(titulo_principal, fontsize=16)
# Subplot 1: Imagen Original (BGR)
fig.add_subplot(1, 3, 1)
plt.title('Formato BGR (Original)')
plt.imshow(img_bgr)
plt.axis('off')
# Subplot 2: Imagen en formato RGB
fig.add_subplot(1, 3, 2)
plt.title('Formato RGB (Corregido)')
plt.imshow(img_rgb)
plt.axis('off')
# Subplot 3: Imagen en Escala de Grises
fig.add_subplot(1, 3, 3)
plt.title('Escala de Grises')
plt.imshow(img_gris, cmap="gray")
plt.axis('off')
plt.show()
# --- Bucle para encontrar y desplegar las imágenes de las placas ---
print("🖼️ Mostrando las imágenes de las placas (Plates)...")
# Iteramos a través de la lista de nombres de archivo de p2b junto con su índice
for i, nombre_archivo in enumerate(nombres_archivos_procesados_p2b):
# Llamamos a la función de visualización para cada imagen
# Usamos el índice 'i' para obtener las imágenes correctas de cada lista p2b
mostrar_comparacion_p2(
img_bgr=imagenes_originales_p2b[i],
img_rgb=imagenes_rgb_p2b[i],
img_gris=imagenes_grises_p2b[i],
titulo_principal=nombres_archivos_procesados_p2b[i]
)
🖼️ Mostrando las imágenes de las placas (Plates)...
# CELDA 5:
# ===================================================================
# PROCESAMIENTO Y DILATACIÓN DE MÚLTIPLES IMÁGENES
# ===================================================================
# Listas para almacenar los resultados de todas las placas
todas_matriculas_gris = []
todas_matriculas_binarias = []
todas_dilataciones_gris = []
todas_dilataciones_binarias = []
print(f"🔄 Procesando {len(imagenes_originales_p2b)} placas...")
print("="*60)
# --- Procesar cada imagen de la lista ---
for idx, imagen_original_color in enumerate(imagenes_originales_p2b):
print(f"Procesando placa {idx + 1}/{len(imagenes_originales_p2b)}...", end=" ")
# --- 1. Convertir a escala de grises ---
matricula_gris = cv2.cvtColor(imagen_original_color, cv2.COLOR_BGR2GRAY)
# --- 2. Binarización de la imagen ---
# Píxeles más oscuros que 127 serán negros, los más claros serán blancos
valor_umbral = 127
_, matricula_binaria = cv2.threshold(matricula_gris, valor_umbral, 255, cv2.THRESH_BINARY)
# --- 3. Aplicación de Dilatación ---
# Kernel de 3x3 para dilatación suave
kernel = np.ones((3, 3), np.uint8)
# Aplicar dilatación a imagen en gris y binaria
dilatacion_gris = cv2.dilate(matricula_gris, kernel, iterations=1)
dilatacion_binaria = cv2.dilate(matricula_binaria, kernel, iterations=1)
# --- 4. Guardar resultados en las listas ---
todas_matriculas_gris.append(matricula_gris)
todas_matriculas_binarias.append(matricula_binaria)
todas_dilataciones_gris.append(dilatacion_gris)
todas_dilataciones_binarias.append(dilatacion_binaria)
print("✅")
print("="*60)
print(f"✅ Procesamiento completado para {len(imagenes_originales_p2b)} placas.")
print("📊 Las imágenes están almacenadas en:")
print(" - todas_matriculas_gris")
print(" - todas_matriculas_binarias")
print(" - todas_dilataciones_gris")
print(" - todas_dilataciones_binarias")
🔄 Procesando 19 placas... ============================================================ Procesando placa 1/19... ✅ Procesando placa 2/19... ✅ Procesando placa 3/19... ✅ Procesando placa 4/19... ✅ Procesando placa 5/19... ✅ Procesando placa 6/19... ✅ Procesando placa 7/19... ✅ Procesando placa 8/19... ✅ Procesando placa 9/19... ✅ Procesando placa 10/19... ✅ Procesando placa 11/19... ✅ Procesando placa 12/19... ✅ Procesando placa 13/19... ✅ Procesando placa 14/19... ✅ Procesando placa 15/19... ✅ Procesando placa 16/19... ✅ Procesando placa 17/19... ✅ Procesando placa 18/19... ✅ Procesando placa 19/19... ✅ ============================================================ ✅ Procesamiento completado para 19 placas. 📊 Las imágenes están almacenadas en: - todas_matriculas_gris - todas_matriculas_binarias - todas_dilataciones_gris - todas_dilataciones_binarias
# CELDA 6:
# ===================================================================
# VISUALIZACIÓN DE TODAS LAS PLACAS
# ===================================================================
num_placas = len(imagenes_originales_p2b)
filas = num_placas
columnas = 4 # Original, Gris, Binaria, Dilatación Binaria
plt.figure(figsize=(16, 4 * num_placas))
for idx in range(num_placas):
# Original
plt.subplot(filas, columnas, idx * columnas + 1)
plt.imshow(cv2.cvtColor(imagenes_originales_p2b[idx], cv2.COLOR_BGR2RGB))
plt.title(f'Placa {idx + 1} - Original')
plt.axis('off')
# Escala de grises
plt.subplot(filas, columnas, idx * columnas + 2)
plt.imshow(todas_matriculas_gris[idx], cmap='gray')
plt.title('Escala de Grises')
plt.axis('off')
# Binarizada
plt.subplot(filas, columnas, idx * columnas + 3)
plt.imshow(todas_matriculas_binarias[idx], cmap='gray')
plt.title('Binarizada')
plt.axis('off')
# Dilatación binaria
plt.subplot(filas, columnas, idx * columnas + 4)
plt.imshow(todas_dilataciones_binarias[idx], cmap='gray')
plt.title('Dilatación')
plt.axis('off')
plt.tight_layout()
plt.show()
print(f"✅ Visualizadas {num_placas} placas procesadas")
✅ Visualizadas 19 placas procesadas
# CELDA 7:
# ===================================================================
# VISUALIZACIÓN COMPARATIVA DE TODAS LAS PLACAS
# ===================================================================
num_placas = len(imagenes_originales_p2b)
print(f"📊 Mostrando comparativa de {num_placas} placas procesadas")
print("="*60)
# Crear una figura con subplots para cada placa
# Cada placa tendrá 5 imágenes (1 fila x 5 columnas), así que necesitamos num_placas filas
fig, axes = plt.subplots(num_placas, 5, figsize=(20, 4 * num_placas))
# Si solo hay una placa, aseguramos que axes sea 2D
if num_placas == 1:
axes = axes.reshape(1, -1)
for idx in range(num_placas):
# --- Columna 1: Original en Color ---
axes[idx, 0].imshow(cv2.cvtColor(imagenes_originales_p2b[idx], cv2.COLOR_BGR2RGB))
axes[idx, 0].set_title(f'Placa {idx + 1} - Original (Color)', fontweight='bold')
axes[idx, 0].axis('off')
# --- Columna 2: Original en Grises ---
axes[idx, 1].imshow(todas_matriculas_gris[idx], cmap='gray')
axes[idx, 1].set_title('Grises (Original)')
axes[idx, 1].axis('off')
# --- Columna 3: Dilatación en Grises ---
axes[idx, 2].imshow(todas_dilataciones_gris[idx], cmap='gray')
axes[idx, 2].set_title('Grises con Dilatación')
axes[idx, 2].axis('off')
# --- Columna 4: Imagen Binaria ---
axes[idx, 3].imshow(todas_matriculas_binarias[idx], cmap='gray')
axes[idx, 3].set_title('Imagen Binaria')
axes[idx, 3].axis('off')
# --- Columna 5: Dilatación Binaria ---
axes[idx, 4].imshow(todas_dilataciones_binarias[idx], cmap='gray')
axes[idx, 4].set_title('Binaria con Dilatación')
axes[idx, 4].axis('off')
plt.tight_layout()
plt.show()
print(f"✅ Visualización completada para {num_placas} placas")
📊 Mostrando comparativa de 19 placas procesadas ============================================================
✅ Visualización completada para 19 placas
# CELDA 8:
# ===================================================================
# VISUALIZACIÓN EN LÍNEA E HISTOGRAMA COMBINADO (BATCH)
# ===================================================================
num_placas = len(imagenes_originales_p2b)
print(f"📊 Mostrando análisis detallado de {num_placas} placas")
print("="*60)
for idx in range(num_placas):
print(f"\n🔍 Analizando Placa {idx + 1}/{num_placas}")
# --- 1. Mostramos las tres imágenes principales en una sola fila ---
plt.figure(figsize=(18, 5))
# Gráfico 1: Imagen Original en escala de grises
plt.subplot(1, 3, 1)
plt.imshow(todas_matriculas_gris[idx], cmap='gray')
plt.title(f'Placa {idx + 1} - Original (Grises)', fontweight='bold')
plt.axis('off')
# Gráfico 2: Imagen con Dilatación
plt.subplot(1, 3, 2)
plt.imshow(todas_dilataciones_gris[idx], cmap='gray')
plt.title('Imagen con Dilatación', fontweight='bold')
plt.axis('off')
# Gráfico 3: Diferencia de Píxeles
diferencia_binaria = cv2.absdiff(todas_matriculas_binarias[idx],
todas_dilataciones_binarias[idx])
plt.subplot(1, 3, 3)
plt.imshow(diferencia_binaria, cmap='gray')
plt.title('Diferencia (Píxeles añadidos)', fontweight='bold')
plt.axis('off')
plt.tight_layout()
plt.show()
# --- 2. Comparación de Histogramas (en un solo gráfico, abajo) ---
plt.figure(figsize=(10, 6))
# Dibujamos ambos histogramas juntos
plt.hist(todas_matriculas_gris[idx].ravel(), 256, range=[0, 256],
label='Original', color='blue', alpha=0.7)
plt.hist(todas_dilataciones_gris[idx].ravel(), 256, range=[0, 256],
label='Con Dilatación', color='red', alpha=0.7)
plt.title(f'Placa {idx + 1} - Comparación de Histogramas',
fontsize=12, fontweight='bold')
plt.xlabel('Intensidad de Píxel')
plt.ylabel('Cantidad de Píxeles')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print("\n" + "="*60)
print(f"✅ Análisis completado para {num_placas} placas")
📊 Mostrando análisis detallado de 19 placas ============================================================ 🔍 Analizando Placa 1/19
🔍 Analizando Placa 2/19
🔍 Analizando Placa 3/19
🔍 Analizando Placa 4/19
🔍 Analizando Placa 5/19
🔍 Analizando Placa 6/19
🔍 Analizando Placa 7/19
🔍 Analizando Placa 8/19
🔍 Analizando Placa 9/19
🔍 Analizando Placa 10/19
🔍 Analizando Placa 11/19
🔍 Analizando Placa 12/19
🔍 Analizando Placa 13/19
🔍 Analizando Placa 14/19
🔍 Analizando Placa 15/19
🔍 Analizando Placa 16/19
🔍 Analizando Placa 17/19
🔍 Analizando Placa 18/19
🔍 Analizando Placa 19/19
============================================================ ✅ Análisis completado para 19 placas
# CELDA 9:
# ===================================================================
# GRADIENTE MORFOLÓGICO + UMBRALIZACIÓN
# ===================================================================
import cv2
import numpy as np
import matplotlib.pyplot as plt
num_placas = len(imagenes_originales_p2b)
print("="*70)
print("🎯 DETECCIÓN POR GRADIENTE MORFOLÓGICO")
print("="*70)
todas_imagenes_binarias = []
for idx in range(num_placas):
print(f"\n{'─'*70}")
print(f"📋 PLACA {idx + 1}/{num_placas}")
print(f"{'─'*70}")
img_original = imagenes_originales_p2b[idx].copy()
# Convertir a escala de grises
img_gris = cv2.cvtColor(img_original, cv2.COLOR_BGR2GRAY)
print(" 🔹 Aplicando filtro bilateral para suavizar...")
img_bilateral = cv2.bilateralFilter(img_gris, 11, 17, 17)
# === GRADIENTE MORFOLÓGICO (detecta cambios de intensidad) ===
print(" 🔹 Calculando gradiente morfológico...")
# Kernel pequeño para detectar bordes
kernel_gradiente = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
# Gradiente = Dilatación - Erosión
img_dilatada_temp = cv2.dilate(img_bilateral, kernel_gradiente, iterations=1)
img_erosionada_temp = cv2.erode(img_bilateral, kernel_gradiente, iterations=1)
img_gradiente = cv2.subtract(img_dilatada_temp, img_erosionada_temp)
# === UMBRALIZACIÓN ADAPTATIVA ===
print(" 🔹 Umbralización adaptativa...")
# Otsu sobre el gradiente (detecta zonas con mucho cambio)
_, img_umbral = cv2.threshold(img_gradiente, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# === CIERRE MORFOLÓGICO (conectar regiones) ===
print(" 🔹 Cierre morfológico agresivo...")
kernel_cierre = cv2.getStructuringElement(cv2.MORPH_RECT, (30, 5))
img_cerrada = cv2.morphologyEx(img_umbral, cv2.MORPH_CLOSE, kernel_cierre, iterations=3)
# === ELIMINAR RUIDO PEQUEÑO ===
print(" 🔹 Eliminando ruido...")
# Encontrar contornos
contornos_temp, _ = cv2.findContours(img_cerrada.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# Crear imagen limpia - solo contornos grandes
img_limpia = np.zeros_like(img_cerrada)
for cnt in contornos_temp:
area = cv2.contourArea(cnt)
if area > 1000: # Mantener solo regiones significativas
cv2.drawContours(img_limpia, [cnt], -1, 255, thickness=cv2.FILLED)
todas_imagenes_binarias.append(img_limpia)
# Diagnóstico
contornos_finales, _ = cv2.findContours(img_limpia.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
print(f" ✓ Regiones detectadas: {len(contornos_finales)}")
print(f"\n{'='*70}")
print(f"✅ Procesamiento completado")
print(f"{'='*70}\n")
# === VISUALIZACIÓN ===
fig, axes = plt.subplots(num_placas, 5, figsize=(25, 5 * num_placas))
if num_placas == 1:
axes = axes.reshape(1, -1)
for idx in range(num_placas):
img_gris = cv2.cvtColor(imagenes_originales_p2b[idx], cv2.COLOR_BGR2GRAY)
img_bilateral = cv2.bilateralFilter(img_gris, 11, 17, 17)
kernel_gradiente = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
img_dilatada_temp = cv2.dilate(img_bilateral, kernel_gradiente, iterations=1)
img_erosionada_temp = cv2.erode(img_bilateral, kernel_gradiente, iterations=1)
img_gradiente = cv2.subtract(img_dilatada_temp, img_erosionada_temp)
_, img_umbral = cv2.threshold(img_gradiente, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# Original
axes[idx, 0].imshow(cv2.cvtColor(imagenes_originales_p2b[idx], cv2.COLOR_BGR2RGB))
axes[idx, 0].set_title(f'Placa {idx + 1}', fontsize=10, fontweight='bold')
axes[idx, 0].axis('off')
# Bilateral
axes[idx, 1].imshow(img_bilateral, cmap='gray')
axes[idx, 1].set_title('Bilateral', fontsize=10)
axes[idx, 1].axis('off')
# Gradiente
axes[idx, 2].imshow(img_gradiente, cmap='gray')
axes[idx, 2].set_title('Gradiente Morfológico', fontsize=10)
axes[idx, 2].axis('off')
# Umbral
axes[idx, 3].imshow(img_umbral, cmap='gray')
axes[idx, 3].set_title('Umbralización', fontsize=10)
axes[idx, 3].axis('off')
# Final
axes[idx, 4].imshow(todas_imagenes_binarias[idx], cmap='gray')
axes[idx, 4].set_title('RESULTADO FINAL', fontsize=10, color='green', fontweight='bold')
axes[idx, 4].axis('off')
plt.tight_layout()
plt.show()
print("Observa la columna 'Gradiente Morfológico' - debería resaltar las placas")
====================================================================== 🎯 DETECCIÓN POR GRADIENTE MORFOLÓGICO ====================================================================== ────────────────────────────────────────────────────────────────────── 📋 PLACA 1/19 ────────────────────────────────────────────────────────────────────── 🔹 Aplicando filtro bilateral para suavizar... 🔹 Calculando gradiente morfológico... 🔹 Umbralización adaptativa... 🔹 Cierre morfológico agresivo... 🔹 Eliminando ruido... ✓ Regiones detectadas: 1 ────────────────────────────────────────────────────────────────────── 📋 PLACA 2/19 ────────────────────────────────────────────────────────────────────── 🔹 Aplicando filtro bilateral para suavizar... 🔹 Calculando gradiente morfológico... 🔹 Umbralización adaptativa... 🔹 Cierre morfológico agresivo... 🔹 Eliminando ruido... ✓ Regiones detectadas: 4 ────────────────────────────────────────────────────────────────────── 📋 PLACA 3/19 ────────────────────────────────────────────────────────────────────── 🔹 Aplicando filtro bilateral para suavizar... 🔹 Calculando gradiente morfológico... 🔹 Umbralización adaptativa... 🔹 Cierre morfológico agresivo... 🔹 Eliminando ruido... ✓ Regiones detectadas: 3 ────────────────────────────────────────────────────────────────────── 📋 PLACA 4/19 ────────────────────────────────────────────────────────────────────── 🔹 Aplicando filtro bilateral para suavizar... 🔹 Calculando gradiente morfológico... 🔹 Umbralización adaptativa... 🔹 Cierre morfológico agresivo... 🔹 Eliminando ruido... ✓ Regiones detectadas: 1 ────────────────────────────────────────────────────────────────────── 📋 PLACA 5/19 ────────────────────────────────────────────────────────────────────── 🔹 Aplicando filtro bilateral para suavizar... 🔹 Calculando gradiente morfológico... 🔹 Umbralización adaptativa... 🔹 Cierre morfológico agresivo... 🔹 Eliminando ruido... ✓ Regiones detectadas: 1 ────────────────────────────────────────────────────────────────────── 📋 PLACA 6/19 ────────────────────────────────────────────────────────────────────── 🔹 Aplicando filtro bilateral para suavizar... 🔹 Calculando gradiente morfológico... 🔹 Umbralización adaptativa... 🔹 Cierre morfológico agresivo... 🔹 Eliminando ruido... ✓ Regiones detectadas: 7 ────────────────────────────────────────────────────────────────────── 📋 PLACA 7/19 ────────────────────────────────────────────────────────────────────── 🔹 Aplicando filtro bilateral para suavizar... 🔹 Calculando gradiente morfológico... 🔹 Umbralización adaptativa... 🔹 Cierre morfológico agresivo... 🔹 Eliminando ruido... ✓ Regiones detectadas: 6 ────────────────────────────────────────────────────────────────────── 📋 PLACA 8/19 ────────────────────────────────────────────────────────────────────── 🔹 Aplicando filtro bilateral para suavizar... 🔹 Calculando gradiente morfológico... 🔹 Umbralización adaptativa... 🔹 Cierre morfológico agresivo... 🔹 Eliminando ruido... ✓ Regiones detectadas: 2 ────────────────────────────────────────────────────────────────────── 📋 PLACA 9/19 ────────────────────────────────────────────────────────────────────── 🔹 Aplicando filtro bilateral para suavizar... 🔹 Calculando gradiente morfológico... 🔹 Umbralización adaptativa... 🔹 Cierre morfológico agresivo... 🔹 Eliminando ruido... ✓ Regiones detectadas: 2 ────────────────────────────────────────────────────────────────────── 📋 PLACA 10/19 ────────────────────────────────────────────────────────────────────── 🔹 Aplicando filtro bilateral para suavizar... 🔹 Calculando gradiente morfológico... 🔹 Umbralización adaptativa... 🔹 Cierre morfológico agresivo... 🔹 Eliminando ruido... ✓ Regiones detectadas: 10 ────────────────────────────────────────────────────────────────────── 📋 PLACA 11/19 ────────────────────────────────────────────────────────────────────── 🔹 Aplicando filtro bilateral para suavizar... 🔹 Calculando gradiente morfológico... 🔹 Umbralización adaptativa... 🔹 Cierre morfológico agresivo... 🔹 Eliminando ruido... ✓ Regiones detectadas: 6 ────────────────────────────────────────────────────────────────────── 📋 PLACA 12/19 ────────────────────────────────────────────────────────────────────── 🔹 Aplicando filtro bilateral para suavizar... 🔹 Calculando gradiente morfológico... 🔹 Umbralización adaptativa... 🔹 Cierre morfológico agresivo... 🔹 Eliminando ruido... ✓ Regiones detectadas: 3 ────────────────────────────────────────────────────────────────────── 📋 PLACA 13/19 ────────────────────────────────────────────────────────────────────── 🔹 Aplicando filtro bilateral para suavizar... 🔹 Calculando gradiente morfológico... 🔹 Umbralización adaptativa... 🔹 Cierre morfológico agresivo... 🔹 Eliminando ruido... ✓ Regiones detectadas: 2 ────────────────────────────────────────────────────────────────────── 📋 PLACA 14/19 ────────────────────────────────────────────────────────────────────── 🔹 Aplicando filtro bilateral para suavizar... 🔹 Calculando gradiente morfológico... 🔹 Umbralización adaptativa... 🔹 Cierre morfológico agresivo... 🔹 Eliminando ruido... ✓ Regiones detectadas: 3 ────────────────────────────────────────────────────────────────────── 📋 PLACA 15/19 ────────────────────────────────────────────────────────────────────── 🔹 Aplicando filtro bilateral para suavizar... 🔹 Calculando gradiente morfológico... 🔹 Umbralización adaptativa... 🔹 Cierre morfológico agresivo... 🔹 Eliminando ruido... ✓ Regiones detectadas: 6 ────────────────────────────────────────────────────────────────────── 📋 PLACA 16/19 ────────────────────────────────────────────────────────────────────── 🔹 Aplicando filtro bilateral para suavizar... 🔹 Calculando gradiente morfológico... 🔹 Umbralización adaptativa... 🔹 Cierre morfológico agresivo... 🔹 Eliminando ruido... ✓ Regiones detectadas: 6 ────────────────────────────────────────────────────────────────────── 📋 PLACA 17/19 ────────────────────────────────────────────────────────────────────── 🔹 Aplicando filtro bilateral para suavizar... 🔹 Calculando gradiente morfológico... 🔹 Umbralización adaptativa... 🔹 Cierre morfológico agresivo... 🔹 Eliminando ruido... ✓ Regiones detectadas: 2 ────────────────────────────────────────────────────────────────────── 📋 PLACA 18/19 ────────────────────────────────────────────────────────────────────── 🔹 Aplicando filtro bilateral para suavizar... 🔹 Calculando gradiente morfológico... 🔹 Umbralización adaptativa... 🔹 Cierre morfológico agresivo... 🔹 Eliminando ruido... ✓ Regiones detectadas: 2 ────────────────────────────────────────────────────────────────────── 📋 PLACA 19/19 ────────────────────────────────────────────────────────────────────── 🔹 Aplicando filtro bilateral para suavizar... 🔹 Calculando gradiente morfológico... 🔹 Umbralización adaptativa... 🔹 Cierre morfológico agresivo... 🔹 Eliminando ruido... ✓ Regiones detectadas: 7 ====================================================================== ✅ Procesamiento completado ======================================================================
Observa la columna 'Gradiente Morfológico' - debería resaltar las placas
# CELDA 10:
# ===================================================================
# DETECCIÓN INTELIGENTE DE REGIÓN DE PLACA
# ===================================================================
import cv2
import numpy as np
import matplotlib.pyplot as plt
num_placas = len(imagenes_originales_p2b)
print("="*70)
print("🎯 DETECCIÓN INTELIGENTE DE REGIÓN DE PLACA")
print("="*70)
todas_imagenes_binarias = []
todas_regiones_placa = [] # Máscaras de región de placa
for idx in range(num_placas):
print(f"\n{'─'*70}")
print(f"📋 PLACA {idx + 1}/{num_placas}")
print(f"{'─'*70}")
img_original = imagenes_originales_p2b[idx].copy()
# === PASO 1: Escala de grises ===
img_gris = cv2.cvtColor(img_original, cv2.COLOR_BGR2GRAY)
# === PASO 2: Ecualización adaptativa (mejor que normal) ===
print(" 🔹 Ecualización adaptativa...")
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
img_clahe = clahe.apply(img_gris)
# === PASO 3: Umbralización de Otsu para encontrar regiones claras ===
print(" 🔹 Binarización de Otsu (detectar región clara de placa)...")
# Las placas suelen ser más claras que el resto del auto
blur = cv2.GaussianBlur(img_clahe, (5, 5), 0)
_, img_otsu = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# === PASO 4: Operaciones morfológicas para limpiar ===
print(" 🔹 Limpieza morfológica...")
# Cerrar huecos pequeños
kernel_cierre = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 5))
img_cerrada = cv2.morphologyEx(img_otsu, cv2.MORPH_CLOSE, kernel_cierre)
# Eliminar ruido pequeño
kernel_apertura = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
img_limpia = cv2.morphologyEx(img_cerrada, cv2.MORPH_OPEN, kernel_apertura)
# === PASO 5: Encontrar contornos y filtrar por características de placa ===
print(" 🔹 Buscando candidatos a placa...")
contornos, _ = cv2.findContours(img_limpia, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# Crear máscara para la región de placa
mascara_placa = np.zeros_like(img_gris)
candidatos = []
for cnt in contornos:
area = cv2.contourArea(cnt)
if area < 2000: # Muy pequeño
continue
(x, y, w, h) = cv2.boundingRect(cnt)
if h == 0:
continue
aspect_ratio = w / float(h)
# Filtro muy permisivo: solo buscar cosas rectangulares horizontales
if aspect_ratio > 1.5 and aspect_ratio < 6.0 and area > 5000:
candidatos.append({
'contorno': cnt,
'area': area,
'ar': aspect_ratio,
'x': x, 'y': y, 'w': w, 'h': h
})
# Tomar el candidato con mejor área y aspect ratio
if candidatos:
# Ordenar por score combinado
for c in candidatos:
score = c['area'] * (1.0 / abs(c['ar'] - 3.5)) # Preferir AR cercano a 3.5
c['score'] = score
candidatos.sort(key=lambda x: x['score'], reverse=True)
mejor = candidatos[0]
print(f" ✓ Región candidata encontrada:")
print(f" - Área: {mejor['area']:.0f}, AR: {mejor['ar']:.2f}")
print(f" - Posición: ({mejor['x']}, {mejor['y']}), Tamaño: {mejor['w']}x{mejor['h']}")
# Expandir un poco la región para capturar toda la placa
margen = 10
x1 = max(0, mejor['x'] - margen)
y1 = max(0, mejor['y'] - margen)
x2 = min(img_gris.shape[1], mejor['x'] + mejor['w'] + margen)
y2 = min(img_gris.shape[0], mejor['y'] + mejor['h'] + margen)
# Crear máscara de la región
mascara_placa[y1:y2, x1:x2] = 255
else:
print(f" ⚠️ No se encontró región candidata - usando toda la imagen")
mascara_placa = np.ones_like(img_gris) * 255
todas_regiones_placa.append(mascara_placa)
# === PASO 6: Aplicar Canny SOLO en la región de placa ===
print(" 🔹 Aplicando Canny en región de interés...")
img_blur = cv2.GaussianBlur(img_gris, (5, 5), 0)
img_canny = cv2.Canny(img_blur, 100, 200)
# Aplicar máscara: solo bordes dentro de la región de placa
img_canny_filtrado = cv2.bitwise_and(img_canny, img_canny, mask=mascara_placa)
todas_imagenes_binarias.append(img_canny_filtrado)
print(f" ✓ Procesamiento completado")
print(f"\n{'='*70}")
print(f"✅ Detección de regiones completada")
print(f"{'='*70}\n")
# === VISUALIZACIÓN ===
fig, axes = plt.subplots(num_placas, 5, figsize=(25, 5 * num_placas))
if num_placas == 1:
axes = axes.reshape(1, -1)
for idx in range(num_placas):
img_gris = cv2.cvtColor(imagenes_originales_p2b[idx], cv2.COLOR_BGR2GRAY)
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
img_clahe = clahe.apply(img_gris)
blur = cv2.GaussianBlur(img_clahe, (5, 5), 0)
_, img_otsu = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# Original
axes[idx, 0].imshow(cv2.cvtColor(imagenes_originales_p2b[idx], cv2.COLOR_BGR2RGB))
axes[idx, 0].set_title(f'Placa {idx + 1} - Original', fontsize=10, fontweight='bold')
axes[idx, 0].axis('off')
# CLAHE
axes[idx, 1].imshow(img_clahe, cmap='gray')
axes[idx, 1].set_title('CLAHE', fontsize=10)
axes[idx, 1].axis('off')
# Otsu
axes[idx, 2].imshow(img_otsu, cmap='gray')
axes[idx, 2].set_title('Otsu (regiones claras)', fontsize=10)
axes[idx, 2].axis('off')
# Máscara de región
axes[idx, 3].imshow(todas_regiones_placa[idx], cmap='gray')
axes[idx, 3].set_title('Región de Placa', fontsize=10, color='blue', fontweight='bold')
axes[idx, 3].axis('off')
# Canny filtrado
axes[idx, 4].imshow(todas_imagenes_binarias[idx], cmap='gray')
axes[idx, 4].set_title('Canny FILTRADO', fontsize=10, color='green', fontweight='bold')
axes[idx, 4].axis('off')
plt.tight_layout()
plt.show()
print("✅ Ahora la columna 'Canny FILTRADO' debería mostrar SOLO bordes de la placa")
======================================================================
🎯 DETECCIÓN INTELIGENTE DE REGIÓN DE PLACA
======================================================================
──────────────────────────────────────────────────────────────────────
📋 PLACA 1/19
──────────────────────────────────────────────────────────────────────
🔹 Ecualización adaptativa...
🔹 Binarización de Otsu (detectar región clara de placa)...
🔹 Limpieza morfológica...
🔹 Buscando candidatos a placa...
⚠️ No se encontró región candidata - usando toda la imagen
🔹 Aplicando Canny en región de interés...
✓ Procesamiento completado
──────────────────────────────────────────────────────────────────────
📋 PLACA 2/19
──────────────────────────────────────────────────────────────────────
🔹 Ecualización adaptativa...
🔹 Binarización de Otsu (detectar región clara de placa)...
🔹 Limpieza morfológica...
🔹 Buscando candidatos a placa...
✓ Región candidata encontrada:
- Área: 64020, AR: 2.82
- Posición: (48, 276), Tamaño: 686x243
🔹 Aplicando Canny en región de interés...
✓ Procesamiento completado
──────────────────────────────────────────────────────────────────────
📋 PLACA 3/19
──────────────────────────────────────────────────────────────────────
🔹 Ecualización adaptativa...
🔹 Binarización de Otsu (detectar región clara de placa)...
🔹 Limpieza morfológica...
🔹 Buscando candidatos a placa...
⚠️ No se encontró región candidata - usando toda la imagen
🔹 Aplicando Canny en región de interés...
✓ Procesamiento completado
──────────────────────────────────────────────────────────────────────
📋 PLACA 4/19
──────────────────────────────────────────────────────────────────────
🔹 Ecualización adaptativa...
🔹 Binarización de Otsu (detectar región clara de placa)...
🔹 Limpieza morfológica...
🔹 Buscando candidatos a placa...
✓ Región candidata encontrada:
- Área: 11132, AR: 4.15
- Posición: (472, 987), Tamaño: 328x79
🔹 Aplicando Canny en región de interés...
✓ Procesamiento completado
──────────────────────────────────────────────────────────────────────
📋 PLACA 5/19
──────────────────────────────────────────────────────────────────────
🔹 Ecualización adaptativa...
🔹 Binarización de Otsu (detectar región clara de placa)...
🔹 Limpieza morfológica...
🔹 Buscando candidatos a placa...
✓ Región candidata encontrada:
- Área: 8704, AR: 5.07
- Posición: (461, 488), Tamaño: 289x57
🔹 Aplicando Canny en región de interés...
✓ Procesamiento completado
──────────────────────────────────────────────────────────────────────
📋 PLACA 6/19
──────────────────────────────────────────────────────────────────────
🔹 Ecualización adaptativa...
🔹 Binarización de Otsu (detectar región clara de placa)...
🔹 Limpieza morfológica...
🔹 Buscando candidatos a placa...
✓ Región candidata encontrada:
- Área: 81550, AR: 3.66
- Posición: (0, 406), Tamaño: 710x194
🔹 Aplicando Canny en región de interés...
✓ Procesamiento completado
──────────────────────────────────────────────────────────────────────
📋 PLACA 7/19
──────────────────────────────────────────────────────────────────────
🔹 Ecualización adaptativa...
🔹 Binarización de Otsu (detectar región clara de placa)...
🔹 Limpieza morfológica...
🔹 Buscando candidatos a placa...
⚠️ No se encontró región candidata - usando toda la imagen
🔹 Aplicando Canny en región de interés...
✓ Procesamiento completado
──────────────────────────────────────────────────────────────────────
📋 PLACA 8/19
──────────────────────────────────────────────────────────────────────
🔹 Ecualización adaptativa...
🔹 Binarización de Otsu (detectar región clara de placa)...
🔹 Limpieza morfológica...
🔹 Buscando candidatos a placa...
✓ Región candidata encontrada:
- Área: 223533, AR: 2.08
- Posición: (0, 0), Tamaño: 800x385
🔹 Aplicando Canny en región de interés...
✓ Procesamiento completado
──────────────────────────────────────────────────────────────────────
📋 PLACA 9/19
──────────────────────────────────────────────────────────────────────
🔹 Ecualización adaptativa...
🔹 Binarización de Otsu (detectar región clara de placa)...
🔹 Limpieza morfológica...
🔹 Buscando candidatos a placa...
✓ Región candidata encontrada:
- Área: 194384, AR: 1.78
- Posición: (0, 0), Tamaño: 747x419
🔹 Aplicando Canny en región de interés...
✓ Procesamiento completado
──────────────────────────────────────────────────────────────────────
📋 PLACA 10/19
──────────────────────────────────────────────────────────────────────
🔹 Ecualización adaptativa...
🔹 Binarización de Otsu (detectar región clara de placa)...
🔹 Limpieza morfológica...
🔹 Buscando candidatos a placa...
✓ Región candidata encontrada:
- Área: 153211, AR: 2.87
- Posición: (0, 485), Tamaño: 800x279
🔹 Aplicando Canny en región de interés...
✓ Procesamiento completado
──────────────────────────────────────────────────────────────────────
📋 PLACA 11/19
──────────────────────────────────────────────────────────────────────
🔹 Ecualización adaptativa...
🔹 Binarización de Otsu (detectar región clara de placa)...
🔹 Limpieza morfológica...
🔹 Buscando candidatos a placa...
✓ Región candidata encontrada:
- Área: 90548, AR: 3.46
- Posición: (0, 439), Tamaño: 800x231
🔹 Aplicando Canny en región de interés...
✓ Procesamiento completado
──────────────────────────────────────────────────────────────────────
📋 PLACA 12/19
──────────────────────────────────────────────────────────────────────
🔹 Ecualización adaptativa...
🔹 Binarización de Otsu (detectar región clara de placa)...
🔹 Limpieza morfológica...
🔹 Buscando candidatos a placa...
⚠️ No se encontró región candidata - usando toda la imagen
🔹 Aplicando Canny en región de interés...
✓ Procesamiento completado
──────────────────────────────────────────────────────────────────────
📋 PLACA 13/19
──────────────────────────────────────────────────────────────────────
🔹 Ecualización adaptativa...
🔹 Binarización de Otsu (detectar región clara de placa)...
🔹 Limpieza morfológica...
🔹 Buscando candidatos a placa...
✓ Región candidata encontrada:
- Área: 40505, AR: 2.80
- Posición: (0, 344), Tamaño: 718x256
🔹 Aplicando Canny en región de interés...
✓ Procesamiento completado
──────────────────────────────────────────────────────────────────────
📋 PLACA 14/19
──────────────────────────────────────────────────────────────────────
🔹 Ecualización adaptativa...
🔹 Binarización de Otsu (detectar región clara de placa)...
🔹 Limpieza morfológica...
🔹 Buscando candidatos a placa...
✓ Región candidata encontrada:
- Área: 222240, AR: 2.29
- Posición: (0, 716), Tamaño: 800x350
🔹 Aplicando Canny en región de interés...
✓ Procesamiento completado
──────────────────────────────────────────────────────────────────────
📋 PLACA 15/19
──────────────────────────────────────────────────────────────────────
🔹 Ecualización adaptativa...
🔹 Binarización de Otsu (detectar región clara de placa)...
🔹 Limpieza morfológica...
🔹 Buscando candidatos a placa...
⚠️ No se encontró región candidata - usando toda la imagen
🔹 Aplicando Canny en región de interés...
✓ Procesamiento completado
──────────────────────────────────────────────────────────────────────
📋 PLACA 16/19
──────────────────────────────────────────────────────────────────────
🔹 Ecualización adaptativa...
🔹 Binarización de Otsu (detectar región clara de placa)...
🔹 Limpieza morfológica...
🔹 Buscando candidatos a placa...
✓ Región candidata encontrada:
- Área: 133722, AR: 2.74
- Posición: (0, 0), Tamaño: 800x292
🔹 Aplicando Canny en región de interés...
✓ Procesamiento completado
──────────────────────────────────────────────────────────────────────
📋 PLACA 17/19
──────────────────────────────────────────────────────────────────────
🔹 Ecualización adaptativa...
🔹 Binarización de Otsu (detectar región clara de placa)...
🔹 Limpieza morfológica...
🔹 Buscando candidatos a placa...
✓ Región candidata encontrada:
- Área: 40505, AR: 2.80
- Posición: (0, 344), Tamaño: 718x256
🔹 Aplicando Canny en región de interés...
✓ Procesamiento completado
──────────────────────────────────────────────────────────────────────
📋 PLACA 18/19
──────────────────────────────────────────────────────────────────────
🔹 Ecualización adaptativa...
🔹 Binarización de Otsu (detectar región clara de placa)...
🔹 Limpieza morfológica...
🔹 Buscando candidatos a placa...
✓ Región candidata encontrada:
- Área: 223533, AR: 2.08
- Posición: (0, 0), Tamaño: 800x385
🔹 Aplicando Canny en región de interés...
✓ Procesamiento completado
──────────────────────────────────────────────────────────────────────
📋 PLACA 19/19
──────────────────────────────────────────────────────────────────────
🔹 Ecualización adaptativa...
🔹 Binarización de Otsu (detectar región clara de placa)...
🔹 Limpieza morfológica...
🔹 Buscando candidatos a placa...
✓ Región candidata encontrada:
- Área: 235416, AR: 1.90
- Posición: (0, 645), Tamaño: 800x421
🔹 Aplicando Canny en región de interés...
✓ Procesamiento completado
======================================================================
✅ Detección de regiones completada
======================================================================
✅ Ahora la columna 'Canny FILTRADO' debería mostrar SOLO bordes de la placa
# CELDA 11:
# ===================================================================
# FINAL: COMBINACIÓN DE OPERACIONES MORFOLÓGICAS
# ===================================================================
num_placas = len(imagenes_originales_p2b)
print("="*70)
print("🔬 PIPELINE MORFOLÓGICO COMPLETO")
print("="*70)
print("Combinando: Apertura → Top-Hat → Gradiente → Cierre → Dilatación")
print("="*70)
todas_dilataciones_binarias = []
imagenes_debug = [] # Para ver pasos intermedios
for idx in range(num_placas):
print(f"\n{'─'*70}")
print(f"📋 PLACA {idx + 1}/{num_placas}")
print(f"{'─'*70}")
img_original = imagenes_originales_p2b[idx].copy()
# PASO 1: Escala de grises
img_gris = cv2.cvtColor(img_original, cv2.COLOR_BGR2GRAY)
# PASO 2: Suavizado bilateral (preserva bordes)
img_blur = cv2.bilateralFilter(img_gris, 11, 17, 17)
print(f" ✓ Filtro bilateral aplicado")
# PASO 3: APERTURA para eliminar ruido pequeño ANTES de Top-Hat
kernel_apertura = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
img_apertura = cv2.morphologyEx(img_blur, cv2.MORPH_OPEN, kernel_apertura, iterations=1)
print(f" ✓ Apertura (limpieza de ruido)")
# PASO 4: TOP-HAT sobre imagen limpia
kernel_tophat = cv2.getStructuringElement(cv2.MORPH_RECT, (35, 18))
img_tophat = cv2.morphologyEx(img_apertura, cv2.MORPH_TOPHAT, kernel_tophat)
print(f" ✓ Top-Hat (kernel 35x18)")
# PASO 5: GRADIENTE MORFOLÓGICO para resaltar bordes
kernel_gradiente = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
img_dilatada_temp = cv2.dilate(img_tophat, kernel_gradiente, iterations=1)
img_erosionada_temp = cv2.erode(img_tophat, kernel_gradiente, iterations=1)
img_gradiente = cv2.subtract(img_dilatada_temp, img_erosionada_temp)
print(f" ✓ Gradiente morfológico")
# PASO 6: Combinar Top-Hat + Gradiente (da más información)
img_combinada = cv2.addWeighted(img_tophat, 0.7, img_gradiente, 0.3, 0)
# PASO 7: Binarización con Otsu
_, img_binaria = cv2.threshold(img_combinada, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
print(f" ✓ Binarización Otsu (umbral={_})")
# PASO 8: CIERRE agresivo para unir caracteres
kernel_cierre = cv2.getStructuringElement(cv2.MORPH_RECT, (30, 5))
img_cerrada = cv2.morphologyEx(img_binaria, cv2.MORPH_CLOSE, kernel_cierre, iterations=3)
print(f" ✓ Cierre (kernel 30x5, 3 iter)")
# PASO 9: APERTURA para eliminar ruido conectado
kernel_limpieza = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
img_limpia = cv2.morphologyEx(img_cerrada, cv2.MORPH_OPEN, kernel_limpieza, iterations=1)
print(f" ✓ Apertura (limpieza final)")
# PASO 10: DILATACIÓN final para expandir región de placa
kernel_dilatacion = cv2.getStructuringElement(cv2.MORPH_RECT, (20, 8))
img_dilatada = cv2.dilate(img_limpia, kernel_dilatacion, iterations=2)
print(f" ✓ Dilatación (kernel 20x8, 2 iter)")
# PASO 11: Filtrar contornos por área mínima
contornos_temp, _ = cv2.findContours(img_dilatada, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
img_final = np.zeros_like(img_dilatada)
for cnt in contornos_temp:
area = cv2.contourArea(cnt)
if area > 2000: # Solo regiones significativas
cv2.drawContours(img_final, [cnt], -1, 255, thickness=cv2.FILLED)
print(f" ✓ Filtrado de contornos pequeños")
todas_dilataciones_binarias.append(img_final)
# Guardar imágenes intermedias para debug
imagenes_debug.append({
'tophat': img_tophat,
'gradiente': img_gradiente,
'combinada': img_combinada,
'binaria': img_binaria,
'cerrada': img_cerrada,
'final': img_final
})
# Diagnóstico
contornos_final, _ = cv2.findContours(img_final, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
print(f" 📊 Contornos finales: {len(contornos_final)}")
if len(contornos_final) > 0:
cnt_mayor = max(contornos_final, key=cv2.contourArea)
area_mayor = cv2.contourArea(cnt_mayor)
(x, y, w, h) = cv2.boundingRect(cnt_mayor)
ar = w / float(h) if h > 0 else 0
print(f" 📊 Mayor: Área={area_mayor:.0f}, AR={ar:.2f}, {w}x{h}")
print(f"\n{'='*70}")
print("✅ Pipeline morfológico completado")
print(f"{'='*70}\n")
# VISUALIZACIÓN EXTENDIDA (6 columnas)
fig, axes = plt.subplots(num_placas, 6, figsize=(30, 5 * num_placas))
if num_placas == 1:
axes = axes.reshape(1, -1)
for idx in range(num_placas):
# Original
axes[idx, 0].imshow(cv2.cvtColor(imagenes_originales_p2b[idx], cv2.COLOR_BGR2RGB))
axes[idx, 0].set_title(f'Placa {idx + 1}', fontsize=10, fontweight='bold')
axes[idx, 0].axis('off')
# Top-Hat
axes[idx, 1].imshow(imagenes_debug[idx]['tophat'], cmap='gray')
axes[idx, 1].set_title('Top-Hat', fontsize=10)
axes[idx, 1].axis('off')
# Gradiente
axes[idx, 2].imshow(imagenes_debug[idx]['gradiente'], cmap='gray')
axes[idx, 2].set_title('Gradiente', fontsize=10)
axes[idx, 2].axis('off')
# Combinada
axes[idx, 3].imshow(imagenes_debug[idx]['combinada'], cmap='gray')
axes[idx, 3].set_title('Combinada', fontsize=10, color='blue')
axes[idx, 3].axis('off')
# Después de cierre
axes[idx, 4].imshow(imagenes_debug[idx]['cerrada'], cmap='gray')
axes[idx, 4].set_title('Cierre', fontsize=10)
axes[idx, 4].axis('off')
# Final
axes[idx, 5].imshow(imagenes_debug[idx]['final'], cmap='gray')
axes[idx, 5].set_title('RESULTADO', fontsize=10, color='green', fontweight='bold')
axes[idx, 5].axis('off')
plt.tight_layout()
plt.show()
print("\n💡 Pipeline completo:")
print(" 1. Bilateral Filter (reduce ruido)")
print(" 2. Apertura (elimina puntos aislados)")
print(" 3. Top-Hat (resalta placas claras)")
print(" 4. Gradiente (resalta bordes)")
print(" 5. Combinación (más información)")
print(" 6. Cierre (une caracteres)")
print(" 7. Apertura (limpia ruido conectado)")
print(" 8. Dilatación (expande región final)")
====================================================================== 🔬 PIPELINE MORFOLÓGICO COMPLETO ====================================================================== Combinando: Apertura → Top-Hat → Gradiente → Cierre → Dilatación ====================================================================== ────────────────────────────────────────────────────────────────────── 📋 PLACA 1/19 ────────────────────────────────────────────────────────────────────── ✓ Filtro bilateral aplicado ✓ Apertura (limpieza de ruido) ✓ Top-Hat (kernel 35x18) ✓ Gradiente morfológico ✓ Binarización Otsu (umbral=32.0) ✓ Cierre (kernel 30x5, 3 iter) ✓ Apertura (limpieza final) ✓ Dilatación (kernel 20x8, 2 iter) ✓ Filtrado de contornos pequeños 📊 Contornos finales: 5 📊 Mayor: Área=342135, AR=1.63, 800x491 ────────────────────────────────────────────────────────────────────── 📋 PLACA 2/19 ────────────────────────────────────────────────────────────────────── ✓ Filtro bilateral aplicado ✓ Apertura (limpieza de ruido) ✓ Top-Hat (kernel 35x18) ✓ Gradiente morfológico ✓ Binarización Otsu (umbral=42.0) ✓ Cierre (kernel 30x5, 3 iter) ✓ Apertura (limpieza final) ✓ Dilatación (kernel 20x8, 2 iter) ✓ Filtrado de contornos pequeños 📊 Contornos finales: 8 📊 Mayor: Área=30228, AR=2.37, 372x157 ────────────────────────────────────────────────────────────────────── 📋 PLACA 3/19 ────────────────────────────────────────────────────────────────────── ✓ Filtro bilateral aplicado ✓ Apertura (limpieza de ruido) ✓ Top-Hat (kernel 35x18) ✓ Gradiente morfológico ✓ Binarización Otsu (umbral=34.0) ✓ Cierre (kernel 30x5, 3 iter) ✓ Apertura (limpieza final) ✓ Dilatación (kernel 20x8, 2 iter) ✓ Filtrado de contornos pequeños 📊 Contornos finales: 8 📊 Mayor: Área=121235, AR=2.08, 800x385 ────────────────────────────────────────────────────────────────────── 📋 PLACA 4/19 ────────────────────────────────────────────────────────────────────── ✓ Filtro bilateral aplicado ✓ Apertura (limpieza de ruido) ✓ Top-Hat (kernel 35x18) ✓ Gradiente morfológico ✓ Binarización Otsu (umbral=40.0) ✓ Cierre (kernel 30x5, 3 iter) ✓ Apertura (limpieza final) ✓ Dilatación (kernel 20x8, 2 iter) ✓ Filtrado de contornos pequeños 📊 Contornos finales: 4 📊 Mayor: Área=500662, AR=0.75, 800x1066 ────────────────────────────────────────────────────────────────────── 📋 PLACA 5/19 ────────────────────────────────────────────────────────────────────── ✓ Filtro bilateral aplicado ✓ Apertura (limpieza de ruido) ✓ Top-Hat (kernel 35x18) ✓ Gradiente morfológico ✓ Binarización Otsu (umbral=35.0) ✓ Cierre (kernel 30x5, 3 iter) ✓ Apertura (limpieza final) ✓ Dilatación (kernel 20x8, 2 iter) ✓ Filtrado de contornos pequeños 📊 Contornos finales: 3 📊 Mayor: Área=339424, AR=1.73, 800x463 ────────────────────────────────────────────────────────────────────── 📋 PLACA 6/19 ────────────────────────────────────────────────────────────────────── ✓ Filtro bilateral aplicado ✓ Apertura (limpieza de ruido) ✓ Top-Hat (kernel 35x18) ✓ Gradiente morfológico ✓ Binarización Otsu (umbral=46.0) ✓ Cierre (kernel 30x5, 3 iter) ✓ Apertura (limpieza final) ✓ Dilatación (kernel 20x8, 2 iter) ✓ Filtrado de contornos pequeños 📊 Contornos finales: 3 📊 Mayor: Área=15934, AR=1.92, 179x93 ────────────────────────────────────────────────────────────────────── 📋 PLACA 7/19 ────────────────────────────────────────────────────────────────────── ✓ Filtro bilateral aplicado ✓ Apertura (limpieza de ruido) ✓ Top-Hat (kernel 35x18) ✓ Gradiente morfológico ✓ Binarización Otsu (umbral=34.0) ✓ Cierre (kernel 30x5, 3 iter) ✓ Apertura (limpieza final) ✓ Dilatación (kernel 20x8, 2 iter) ✓ Filtrado de contornos pequeños 📊 Contornos finales: 3 📊 Mayor: Área=294962, AR=1.02, 757x744 ────────────────────────────────────────────────────────────────────── 📋 PLACA 8/19 ────────────────────────────────────────────────────────────────────── ✓ Filtro bilateral aplicado ✓ Apertura (limpieza de ruido) ✓ Top-Hat (kernel 35x18) ✓ Gradiente morfológico ✓ Binarización Otsu (umbral=36.0) ✓ Cierre (kernel 30x5, 3 iter) ✓ Apertura (limpieza final) ✓ Dilatación (kernel 20x8, 2 iter) ✓ Filtrado de contornos pequeños 📊 Contornos finales: 16 📊 Mayor: Área=81797, AR=0.85, 430x505 ────────────────────────────────────────────────────────────────────── 📋 PLACA 9/19 ────────────────────────────────────────────────────────────────────── ✓ Filtro bilateral aplicado ✓ Apertura (limpieza de ruido) ✓ Top-Hat (kernel 35x18) ✓ Gradiente morfológico ✓ Binarización Otsu (umbral=41.0) ✓ Cierre (kernel 30x5, 3 iter) ✓ Apertura (limpieza final) ✓ Dilatación (kernel 20x8, 2 iter) ✓ Filtrado de contornos pequeños 📊 Contornos finales: 3 📊 Mayor: Área=89038, AR=3.53, 684x194 ────────────────────────────────────────────────────────────────────── 📋 PLACA 10/19 ────────────────────────────────────────────────────────────────────── ✓ Filtro bilateral aplicado ✓ Apertura (limpieza de ruido) ✓ Top-Hat (kernel 35x18) ✓ Gradiente morfológico ✓ Binarización Otsu (umbral=32.0) ✓ Cierre (kernel 30x5, 3 iter) ✓ Apertura (limpieza final) ✓ Dilatación (kernel 20x8, 2 iter) ✓ Filtrado de contornos pequeños 📊 Contornos finales: 13 📊 Mayor: Área=23109, AR=8.25, 800x97 ────────────────────────────────────────────────────────────────────── 📋 PLACA 11/19 ────────────────────────────────────────────────────────────────────── ✓ Filtro bilateral aplicado ✓ Apertura (limpieza de ruido) ✓ Top-Hat (kernel 35x18) ✓ Gradiente morfológico ✓ Binarización Otsu (umbral=45.0) ✓ Cierre (kernel 30x5, 3 iter) ✓ Apertura (limpieza final) ✓ Dilatación (kernel 20x8, 2 iter) ✓ Filtrado de contornos pequeños 📊 Contornos finales: 5 📊 Mayor: Área=65216, AR=2.81, 595x212 ────────────────────────────────────────────────────────────────────── 📋 PLACA 12/19 ────────────────────────────────────────────────────────────────────── ✓ Filtro bilateral aplicado ✓ Apertura (limpieza de ruido) ✓ Top-Hat (kernel 35x18) ✓ Gradiente morfológico ✓ Binarización Otsu (umbral=51.0) ✓ Cierre (kernel 30x5, 3 iter) ✓ Apertura (limpieza final) ✓ Dilatación (kernel 20x8, 2 iter) ✓ Filtrado de contornos pequeños 📊 Contornos finales: 6 📊 Mayor: Área=103242, AR=4.42, 800x181 ────────────────────────────────────────────────────────────────────── 📋 PLACA 13/19 ────────────────────────────────────────────────────────────────────── ✓ Filtro bilateral aplicado ✓ Apertura (limpieza de ruido) ✓ Top-Hat (kernel 35x18) ✓ Gradiente morfológico ✓ Binarización Otsu (umbral=42.0) ✓ Cierre (kernel 30x5, 3 iter) ✓ Apertura (limpieza final) ✓ Dilatación (kernel 20x8, 2 iter) ✓ Filtrado de contornos pequeños 📊 Contornos finales: 11 📊 Mayor: Área=39642, AR=3.26, 456x140 ────────────────────────────────────────────────────────────────────── 📋 PLACA 14/19 ────────────────────────────────────────────────────────────────────── ✓ Filtro bilateral aplicado ✓ Apertura (limpieza de ruido) ✓ Top-Hat (kernel 35x18) ✓ Gradiente morfológico ✓ Binarización Otsu (umbral=34.0) ✓ Cierre (kernel 30x5, 3 iter) ✓ Apertura (limpieza final) ✓ Dilatación (kernel 20x8, 2 iter) ✓ Filtrado de contornos pequeños 📊 Contornos finales: 8 📊 Mayor: Área=168199, AR=2.38, 682x287 ────────────────────────────────────────────────────────────────────── 📋 PLACA 15/19 ────────────────────────────────────────────────────────────────────── ✓ Filtro bilateral aplicado ✓ Apertura (limpieza de ruido) ✓ Top-Hat (kernel 35x18) ✓ Gradiente morfológico ✓ Binarización Otsu (umbral=34.0) ✓ Cierre (kernel 30x5, 3 iter) ✓ Apertura (limpieza final) ✓ Dilatación (kernel 20x8, 2 iter) ✓ Filtrado de contornos pequeños 📊 Contornos finales: 3 📊 Mayor: Área=294962, AR=1.02, 757x744 ────────────────────────────────────────────────────────────────────── 📋 PLACA 16/19 ────────────────────────────────────────────────────────────────────── ✓ Filtro bilateral aplicado ✓ Apertura (limpieza de ruido) ✓ Top-Hat (kernel 35x18) ✓ Gradiente morfológico ✓ Binarización Otsu (umbral=38.0) ✓ Cierre (kernel 30x5, 3 iter) ✓ Apertura (limpieza final) ✓ Dilatación (kernel 20x8, 2 iter) ✓ Filtrado de contornos pequeños 📊 Contornos finales: 9 📊 Mayor: Área=73124, AR=2.81, 585x208 ────────────────────────────────────────────────────────────────────── 📋 PLACA 17/19 ────────────────────────────────────────────────────────────────────── ✓ Filtro bilateral aplicado ✓ Apertura (limpieza de ruido) ✓ Top-Hat (kernel 35x18) ✓ Gradiente morfológico ✓ Binarización Otsu (umbral=42.0) ✓ Cierre (kernel 30x5, 3 iter) ✓ Apertura (limpieza final) ✓ Dilatación (kernel 20x8, 2 iter) ✓ Filtrado de contornos pequeños 📊 Contornos finales: 11 📊 Mayor: Área=39642, AR=3.26, 456x140 ────────────────────────────────────────────────────────────────────── 📋 PLACA 18/19 ────────────────────────────────────────────────────────────────────── ✓ Filtro bilateral aplicado ✓ Apertura (limpieza de ruido) ✓ Top-Hat (kernel 35x18) ✓ Gradiente morfológico ✓ Binarización Otsu (umbral=36.0) ✓ Cierre (kernel 30x5, 3 iter) ✓ Apertura (limpieza final) ✓ Dilatación (kernel 20x8, 2 iter) ✓ Filtrado de contornos pequeños 📊 Contornos finales: 16 📊 Mayor: Área=81797, AR=0.85, 430x505 ────────────────────────────────────────────────────────────────────── 📋 PLACA 19/19 ────────────────────────────────────────────────────────────────────── ✓ Filtro bilateral aplicado ✓ Apertura (limpieza de ruido) ✓ Top-Hat (kernel 35x18) ✓ Gradiente morfológico ✓ Binarización Otsu (umbral=25.0) ✓ Cierre (kernel 30x5, 3 iter) ✓ Apertura (limpieza final) ✓ Dilatación (kernel 20x8, 2 iter) ✓ Filtrado de contornos pequeños 📊 Contornos finales: 7 📊 Mayor: Área=242986, AR=1.75, 800x456 ====================================================================== ✅ Pipeline morfológico completado ======================================================================
💡 Pipeline completo: 1. Bilateral Filter (reduce ruido) 2. Apertura (elimina puntos aislados) 3. Top-Hat (resalta placas claras) 4. Gradiente (resalta bordes) 5. Combinación (más información) 6. Cierre (une caracteres) 7. Apertura (limpia ruido conectado) 8. Dilatación (expande región final)
# CELDA 11:
# ===================================================================
# CELDA 5: FILTRADO CON ASPECT RATIO CORREGIDO
# ===================================================================
import numpy as np
# Filtros más permisivos basados en el diagnóstico
FILTRO_AREA_MIN = 18000 # Reducido de 30000 (Placa 1 tiene 29894)
FILTRO_ASPECT_RATIO_MIN = 1.9 # Más permisivo
FILTRO_ASPECT_RATIO_MAX = 4.0 # Ampliado para capturar Placa 1 (AR=8.01)
FILTRO_SOLIDEZ_MIN = 0.25 # Ligeramente más permisivo
num_placas = len(imagenes_originales_p2b)
print("="*70)
print("🔍 APLICANDO FILTROS PARA PLACAS HORIZONTALES")
print("="*70)
print(f"📋 Filtros configurados:")
print(f" - Área mínima: {FILTRO_AREA_MIN}")
print(f" - Aspect Ratio (ancho/alto): {FILTRO_ASPECT_RATIO_MIN} - {FILTRO_ASPECT_RATIO_MAX}")
print(f" - Solidez mínima: {FILTRO_SOLIDEZ_MIN}")
print("="*70)
mejores_contornos_placas = []
resultados_filtrado = []
for idx in range(num_placas):
print(f"\n{'─'*70}")
print(f"📋 PLACA {idx + 1}/{num_placas}")
print(f"{'─'*70}")
contornos, _ = cv2.findContours(todas_dilataciones_binarias[idx].copy(),
cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
mejor_placa_contorno = None
mejor_score = 0
candidatos_pasaron = 0
print(f" 📊 Total de contornos: {len(contornos)}")
for contorno in contornos:
area = cv2.contourArea(contorno)
(x, y, w, h) = cv2.boundingRect(contorno)
if h == 0 or w == 0:
continue
aspect_ratio = w / float(h)
solidez = area / (w * h)
# Filtros para placas HORIZONTALES
if (area > FILTRO_AREA_MIN and
aspect_ratio > FILTRO_ASPECT_RATIO_MIN and
aspect_ratio < FILTRO_ASPECT_RATIO_MAX and
solidez > FILTRO_SOLIDEZ_MIN):
candidatos_pasaron += 1
# Sistema de scoring
score = (area / 100000) * 30
aspect_ideal = 3.5
aspect_diff = abs(aspect_ratio - aspect_ideal)
score += max(0, (1 - aspect_diff/2) * 40)
score += solidez * 30
print(f" ✓ Candidato #{candidatos_pasaron}:")
print(f" - Área: {area:.0f}, AR: {aspect_ratio:.2f}, Solidez: {solidez:.2f}, Score: {score:.1f}")
if score > mejor_score:
mejor_score = score
mejor_placa_contorno = contorno
mejores_contornos_placas.append(mejor_placa_contorno)
if mejor_placa_contorno is not None:
area_final = cv2.contourArea(mejor_placa_contorno)
print(f"\n ✅ Placa detectada - Área: {area_final:.0f}, Score: {mejor_score:.1f}")
resultados_filtrado.append({
'placa': idx + 1,
'estado': 'ÉXITO',
'area': area_final,
'score': mejor_score,
'validos': candidatos_pasaron
})
else:
print(f"\n ❌ No detectada - {candidatos_pasaron} candidatos pasaron filtros")
resultados_filtrado.append({
'placa': idx + 1,
'estado': 'FALLO',
'area': 0,
'score': 0,
'validos': candidatos_pasaron
})
# Resumen
print(f"\n{'='*70}")
print(f"📊 RESUMEN")
print(f"{'='*70}")
placas_exitosas = sum(1 for r in resultados_filtrado if r['estado'] == 'ÉXITO')
print(f"✅ Detectadas: {placas_exitosas}/{num_placas}")
print(f"❌ No detectadas: {num_placas - placas_exitosas}/{num_placas}")
# Visualización
fig, axes = plt.subplots(num_placas, 2, figsize=(16, 8 * num_placas))
if num_placas == 1:
axes = axes.reshape(1, -1)
for idx in range(num_placas):
axes[idx, 0].imshow(cv2.cvtColor(imagenes_originales_p2b[idx], cv2.COLOR_BGR2RGB))
axes[idx, 0].set_title(f'Placa {idx + 1} - Original', fontsize=12)
axes[idx, 0].axis('off')
imagen_resultado = imagenes_originales_p2b[idx].copy()
if mejores_contornos_placas[idx] is not None:
cv2.drawContours(imagen_resultado, [mejores_contornos_placas[idx]], -1, (0, 255, 0), 3)
(x, y, w, h) = cv2.boundingRect(mejores_contornos_placas[idx])
cv2.rectangle(imagen_resultado, (x, y), (x + w, y + h), (0, 255, 0), 2)
titulo = f'[OK] Detectada (Score: {resultados_filtrado[idx]["score"]:.1f})'
color = 'green'
else:
titulo = '[X] No Detectada'
color = 'red'
axes[idx, 1].imshow(cv2.cvtColor(imagen_resultado, cv2.COLOR_BGR2RGB))
axes[idx, 1].set_title(titulo, fontsize=12, color=color, fontweight='bold')
axes[idx, 1].axis('off')
plt.tight_layout()
plt.show()
======================================================================
🔍 APLICANDO FILTROS PARA PLACAS HORIZONTALES
======================================================================
📋 Filtros configurados:
- Área mínima: 18000
- Aspect Ratio (ancho/alto): 1.9 - 4.0
- Solidez mínima: 0.25
======================================================================
──────────────────────────────────────────────────────────────────────
📋 PLACA 1/19
──────────────────────────────────────────────────────────────────────
📊 Total de contornos: 5
✓ Candidato #1:
- Área: 180102, AR: 2.18, Solidez: 0.61, Score: 86.0
✅ Placa detectada - Área: 180102, Score: 86.0
──────────────────────────────────────────────────────────────────────
📋 PLACA 2/19
──────────────────────────────────────────────────────────────────────
📊 Total de contornos: 8
✓ Candidato #1:
- Área: 30228, AR: 2.37, Solidez: 0.52, Score: 42.0
✅ Placa detectada - Área: 30228, Score: 42.0
──────────────────────────────────────────────────────────────────────
📋 PLACA 3/19
──────────────────────────────────────────────────────────────────────
📊 Total de contornos: 8
✓ Candidato #1:
- Área: 21862, AR: 3.20, Solidez: 0.74, Score: 62.8
✓ Candidato #2:
- Área: 121235, AR: 2.08, Solidez: 0.39, Score: 59.7
✅ Placa detectada - Área: 21862, Score: 62.8
──────────────────────────────────────────────────────────────────────
📋 PLACA 4/19
──────────────────────────────────────────────────────────────────────
📊 Total de contornos: 4
❌ No detectada - 0 candidatos pasaron filtros
──────────────────────────────────────────────────────────────────────
📋 PLACA 5/19
──────────────────────────────────────────────────────────────────────
📊 Total de contornos: 3
✓ Candidato #1:
- Área: 23053, AR: 3.87, Solidez: 0.47, Score: 53.6
✅ Placa detectada - Área: 23053, Score: 53.6
──────────────────────────────────────────────────────────────────────
📋 PLACA 6/19
──────────────────────────────────────────────────────────────────────
📊 Total de contornos: 3
❌ No detectada - 0 candidatos pasaron filtros
──────────────────────────────────────────────────────────────────────
📋 PLACA 7/19
──────────────────────────────────────────────────────────────────────
📊 Total de contornos: 3
❌ No detectada - 0 candidatos pasaron filtros
──────────────────────────────────────────────────────────────────────
📋 PLACA 8/19
──────────────────────────────────────────────────────────────────────
📊 Total de contornos: 16
❌ No detectada - 0 candidatos pasaron filtros
──────────────────────────────────────────────────────────────────────
📋 PLACA 9/19
──────────────────────────────────────────────────────────────────────
📊 Total de contornos: 3
✓ Candidato #1:
- Área: 89038, AR: 3.53, Solidez: 0.67, Score: 86.3
✅ Placa detectada - Área: 89038, Score: 86.3
──────────────────────────────────────────────────────────────────────
📋 PLACA 10/19
──────────────────────────────────────────────────────────────────────
📊 Total de contornos: 13
✓ Candidato #1:
- Área: 19966, AR: 2.04, Solidez: 0.40, Score: 28.8
✅ Placa detectada - Área: 19966, Score: 28.8
──────────────────────────────────────────────────────────────────────
📋 PLACA 11/19
──────────────────────────────────────────────────────────────────────
📊 Total de contornos: 5
✓ Candidato #1:
- Área: 65216, AR: 2.81, Solidez: 0.52, Score: 61.2
✓ Candidato #2:
- Área: 36842, AR: 3.12, Solidez: 0.45, Score: 57.2
✅ Placa detectada - Área: 65216, Score: 61.2
──────────────────────────────────────────────────────────────────────
📋 PLACA 12/19
──────────────────────────────────────────────────────────────────────
📊 Total de contornos: 6
✓ Candidato #1:
- Área: 28404, AR: 1.90, Solidez: 0.31, Score: 25.8
✅ Placa detectada - Área: 28404, Score: 25.8
──────────────────────────────────────────────────────────────────────
📋 PLACA 13/19
──────────────────────────────────────────────────────────────────────
📊 Total de contornos: 11
✓ Candidato #1:
- Área: 39642, AR: 3.26, Solidez: 0.62, Score: 65.7
✅ Placa detectada - Área: 39642, Score: 65.7
──────────────────────────────────────────────────────────────────────
📋 PLACA 14/19
──────────────────────────────────────────────────────────────────────
📊 Total de contornos: 8
✓ Candidato #1:
- Área: 168199, AR: 2.38, Solidez: 0.86, Score: 93.8
✓ Candidato #2:
- Área: 78872, AR: 2.71, Solidez: 0.44, Score: 60.9
✅ Placa detectada - Área: 168199, Score: 93.8
──────────────────────────────────────────────────────────────────────
📋 PLACA 15/19
──────────────────────────────────────────────────────────────────────
📊 Total de contornos: 3
❌ No detectada - 0 candidatos pasaron filtros
──────────────────────────────────────────────────────────────────────
📋 PLACA 16/19
──────────────────────────────────────────────────────────────────────
📊 Total de contornos: 9
✓ Candidato #1:
- Área: 73124, AR: 2.81, Solidez: 0.60, Score: 66.2
✅ Placa detectada - Área: 73124, Score: 66.2
──────────────────────────────────────────────────────────────────────
📋 PLACA 17/19
──────────────────────────────────────────────────────────────────────
📊 Total de contornos: 11
✓ Candidato #1:
- Área: 39642, AR: 3.26, Solidez: 0.62, Score: 65.7
✅ Placa detectada - Área: 39642, Score: 65.7
──────────────────────────────────────────────────────────────────────
📋 PLACA 18/19
──────────────────────────────────────────────────────────────────────
📊 Total de contornos: 16
❌ No detectada - 0 candidatos pasaron filtros
──────────────────────────────────────────────────────────────────────
📋 PLACA 19/19
──────────────────────────────────────────────────────────────────────
📊 Total de contornos: 7
✓ Candidato #1:
- Área: 81900, AR: 3.86, Solidez: 0.49, Score: 72.1
✓ Candidato #2:
- Área: 32714, AR: 3.13, Solidez: 0.36, Score: 53.1
✅ Placa detectada - Área: 81900, Score: 72.1
======================================================================
📊 RESUMEN
======================================================================
✅ Detectadas: 13/19
❌ No detectadas: 6/19
# CELDA 12:
# ===================================================================
# RECORTE DE PLACAS DETECTADAS
# ===================================================================
num_placas = len(imagenes_originales_p2b)
print("="*70)
print("📸 RECORTANDO PLACAS DETECTADAS")
print("="*70)
# Listas para almacenar recortes
todas_placas_recortadas_color = []
todas_placas_recortadas_gris = [] # CAMBIADO: gris en lugar de binaria
indices_exitosos = []
for idx in range(num_placas):
if mejores_contornos_placas[idx] is not None:
(x, y, w, h) = cv2.boundingRect(mejores_contornos_placas[idx])
# Recortar de la imagen ORIGINAL en color
placa_recortada_color = imagenes_originales_p2b[idx][y:y+h, x:x+w]
# Recortar en ESCALA DE GRISES (no binaria)
img_gris = cv2.cvtColor(imagenes_originales_p2b[idx], cv2.COLOR_BGR2GRAY)
placa_recortada_gris = img_gris[y:y+h, x:x+w]
todas_placas_recortadas_color.append(placa_recortada_color)
todas_placas_recortadas_gris.append(placa_recortada_gris)
indices_exitosos.append(idx)
print(f"✅ Placa {idx + 1}: Recortada ({w}x{h} px)")
else:
# Si no se detectó, agregar None
todas_placas_recortadas_color.append(None)
todas_placas_recortadas_gris.append(None)
print(f"❌ Placa {idx + 1}: No detectada")
num_exitosas = len(indices_exitosos)
print(f"\n{'='*70}")
print(f"📊 {num_exitosas} de {num_placas} placas recortadas exitosamente")
print(f"{'='*70}\n")
# Visualización
if num_exitosas > 0:
fig, axes = plt.subplots(num_exitosas, 3, figsize=(18, 6 * num_exitosas))
if num_exitosas == 1:
axes = axes.reshape(1, -1)
for i, idx in enumerate(indices_exitosos):
# Imagen con rectángulo
imagen_con_placa = imagenes_originales_p2b[idx].copy()
(x, y, w, h) = cv2.boundingRect(mejores_contornos_placas[idx])
cv2.rectangle(imagen_con_placa, (x, y), (x + w, y + h), (0, 255, 0), 3)
axes[i, 0].imshow(cv2.cvtColor(imagen_con_placa, cv2.COLOR_BGR2RGB))
axes[i, 0].set_title(f'Placa {idx + 1} - Detectada', fontweight='bold')
axes[i, 0].axis('off')
axes[i, 1].imshow(cv2.cvtColor(todas_placas_recortadas_color[idx], cv2.COLOR_BGR2RGB))
axes[i, 1].set_title('Recorte (Color)', fontweight='bold')
axes[i, 1].axis('off')
axes[i, 2].imshow(todas_placas_recortadas_gris[idx], cmap='gray')
axes[i, 2].set_title('Recorte (Grises)', fontweight='bold')
axes[i, 2].axis('off')
plt.tight_layout()
plt.show()
print("✅ Visualización completada")
else:
print("❌ No hay placas exitosas para visualizar")
====================================================================== 📸 RECORTANDO PLACAS DETECTADAS ====================================================================== ✅ Placa 1: Recortada (800x367 px) ✅ Placa 2: Recortada (372x157 px) ✅ Placa 3: Recortada (307x96 px) ❌ Placa 4: No detectada ✅ Placa 5: Recortada (437x113 px) ❌ Placa 6: No detectada ❌ Placa 7: No detectada ❌ Placa 8: No detectada ✅ Placa 9: Recortada (684x194 px) ✅ Placa 10: Recortada (318x156 px) ✅ Placa 11: Recortada (595x212 px) ✅ Placa 12: Recortada (421x221 px) ✅ Placa 13: Recortada (456x140 px) ✅ Placa 14: Recortada (682x287 px) ❌ Placa 15: No detectada ✅ Placa 16: Recortada (585x208 px) ✅ Placa 17: Recortada (456x140 px) ❌ Placa 18: No detectada ✅ Placa 19: Recortada (800x207 px) ====================================================================== 📊 13 de 19 placas recortadas exitosamente ======================================================================
✅ Visualización completada
# CELDA 13:
# ===================================================================
# ROTACIÓN PRECISA (BATCH) - VERSIÓN DEFINITIVA
# ===================================================================
if 'todas_placas_recortadas_color' not in locals() or 'todas_placas_recortadas_gris' not in locals():
print("❌ ERROR: No se encontraron las placas recortadas.")
print(" Por favor, ejecuta primero la CELDA 6.")
else:
num_placas = len(imagenes_originales_p2b)
print("="*70)
print("🔄 CORRECCIÓN DE ROTACIÓN - TODAS LAS PLACAS")
print("="*70)
todas_placas_enderezadas = []
placas_enderezadas_exitosas = 0
placas_sin_enderezar = 0
for idx in range(num_placas):
print(f"\n{'─'*70}")
print(f"📋 PLACA {idx + 1}/{num_placas}")
if todas_placas_recortadas_gris[idx] is not None:
try:
placa_gris = todas_placas_recortadas_gris[idx]
(h, w) = placa_gris.shape
print(f" 📐 Dimensiones: {w}x{h}")
# ESTRATEGIA SIMPLE: Verificar si está horizontal o vertical
# Las placas deben ser más anchas que altas
if w < h:
# Está vertical - rotar 90 grados
placa_enderezada = cv2.rotate(placa_gris, cv2.ROTATE_90_CLOCKWISE)
print(f" 🔄 Placa vertical detectada - rotando 90°")
placas_enderezadas_exitosas += 1
else:
# Ya está horizontal - no rotar
placa_enderezada = placa_gris.copy()
print(f" ✓ Placa ya está horizontal")
placas_enderezadas_exitosas += 1
todas_placas_enderezadas.append(placa_enderezada)
(h_final, w_final) = placa_enderezada.shape
print(f" 📊 Dimensiones finales: {w_final}x{h_final}")
except Exception as e:
todas_placas_enderezadas.append(None)
print(f" ❌ Error: {str(e)}")
placas_sin_enderezar += 1
else:
todas_placas_enderezadas.append(None)
print(f" ❌ No hay placa recortada")
placas_sin_enderezar += 1
print(f"\n{'='*70}")
print(f"📊 RESUMEN")
print(f"{'='*70}")
print(f"✅ Placas procesadas: {placas_enderezadas_exitosas}/{num_placas}")
print(f"❌ Placas sin procesar: {placas_sin_enderezar}/{num_placas}")
print(f"{'='*70}\n")
# VISUALIZACIÓN
if placas_enderezadas_exitosas > 0:
print("📊 Mostrando comparación antes/después...\n")
placas_validas = sum(1 for p in todas_placas_enderezadas if p is not None)
fig, axes = plt.subplots(placas_validas, 2, figsize=(14, 6 * placas_validas))
if placas_validas == 1:
axes = axes.reshape(1, -1)
fila_actual = 0
for idx in range(num_placas):
if todas_placas_enderezadas[idx] is not None:
axes[fila_actual, 0].imshow(todas_placas_recortadas_gris[idx], cmap='gray')
axes[fila_actual, 0].set_title(f'Placa {idx + 1} - Recortada',
fontsize=12, fontweight='bold')
axes[fila_actual, 0].axis('off')
axes[fila_actual, 1].imshow(todas_placas_enderezadas[idx], cmap='gray')
axes[fila_actual, 1].set_title('Placa Procesada',
fontsize=12, fontweight='bold', color='green')
axes[fila_actual, 1].axis('off')
fila_actual += 1
plt.tight_layout()
plt.show()
print("✅ Visualización completada")
else:
print("⚠️ No hay placas procesadas")
print("\n✅ Procesamiento completado")
====================================================================== 🔄 CORRECCIÓN DE ROTACIÓN - TODAS LAS PLACAS ====================================================================== ────────────────────────────────────────────────────────────────────── 📋 PLACA 1/19 📐 Dimensiones: 800x367 ✓ Placa ya está horizontal 📊 Dimensiones finales: 800x367 ────────────────────────────────────────────────────────────────────── 📋 PLACA 2/19 📐 Dimensiones: 372x157 ✓ Placa ya está horizontal 📊 Dimensiones finales: 372x157 ────────────────────────────────────────────────────────────────────── 📋 PLACA 3/19 📐 Dimensiones: 307x96 ✓ Placa ya está horizontal 📊 Dimensiones finales: 307x96 ────────────────────────────────────────────────────────────────────── 📋 PLACA 4/19 ❌ No hay placa recortada ────────────────────────────────────────────────────────────────────── 📋 PLACA 5/19 📐 Dimensiones: 437x113 ✓ Placa ya está horizontal 📊 Dimensiones finales: 437x113 ────────────────────────────────────────────────────────────────────── 📋 PLACA 6/19 ❌ No hay placa recortada ────────────────────────────────────────────────────────────────────── 📋 PLACA 7/19 ❌ No hay placa recortada ────────────────────────────────────────────────────────────────────── 📋 PLACA 8/19 ❌ No hay placa recortada ────────────────────────────────────────────────────────────────────── 📋 PLACA 9/19 📐 Dimensiones: 684x194 ✓ Placa ya está horizontal 📊 Dimensiones finales: 684x194 ────────────────────────────────────────────────────────────────────── 📋 PLACA 10/19 📐 Dimensiones: 318x156 ✓ Placa ya está horizontal 📊 Dimensiones finales: 318x156 ────────────────────────────────────────────────────────────────────── 📋 PLACA 11/19 📐 Dimensiones: 595x212 ✓ Placa ya está horizontal 📊 Dimensiones finales: 595x212 ────────────────────────────────────────────────────────────────────── 📋 PLACA 12/19 📐 Dimensiones: 421x221 ✓ Placa ya está horizontal 📊 Dimensiones finales: 421x221 ────────────────────────────────────────────────────────────────────── 📋 PLACA 13/19 📐 Dimensiones: 456x140 ✓ Placa ya está horizontal 📊 Dimensiones finales: 456x140 ────────────────────────────────────────────────────────────────────── 📋 PLACA 14/19 📐 Dimensiones: 682x287 ✓ Placa ya está horizontal 📊 Dimensiones finales: 682x287 ────────────────────────────────────────────────────────────────────── 📋 PLACA 15/19 ❌ No hay placa recortada ────────────────────────────────────────────────────────────────────── 📋 PLACA 16/19 📐 Dimensiones: 585x208 ✓ Placa ya está horizontal 📊 Dimensiones finales: 585x208 ────────────────────────────────────────────────────────────────────── 📋 PLACA 17/19 📐 Dimensiones: 456x140 ✓ Placa ya está horizontal 📊 Dimensiones finales: 456x140 ────────────────────────────────────────────────────────────────────── 📋 PLACA 18/19 ❌ No hay placa recortada ────────────────────────────────────────────────────────────────────── 📋 PLACA 19/19 📐 Dimensiones: 800x207 ✓ Placa ya está horizontal 📊 Dimensiones finales: 800x207 ====================================================================== 📊 RESUMEN ====================================================================== ✅ Placas procesadas: 13/19 ❌ Placas sin procesar: 6/19 ====================================================================== 📊 Mostrando comparación antes/después...
✅ Visualización completada ✅ Procesamiento completado
# CELDA 14:
# ===================================================================
# DETECCIÓN Y EXTRACCIÓN DE PLACAS (BATCH)
# ===================================================================
# Verificar que existan las variables necesarias
if 'imagenes_originales_p2b' not in locals():
print("❌ ERROR: No se encontraron las imágenes originales.")
print(" Por favor, ejecuta primero las celdas anteriores.")
elif 'todas_dilataciones_binarias' not in locals():
print("❌ ERROR: No se encontraron las máscaras morfológicas finales.")
print(" Por favor, ejecuta primero la CELDA 4.")
else:
print("="*70)
print("🔍 DETECCIÓN Y EXTRACCIÓN DE PLACAS")
print("="*70)
# Parámetros de detección
AREA_MIN = 15000
AR_MIN = 1.5
AR_MAX = 8.0
SOLIDEZ_MIN = 0.20
num_placas = len(imagenes_originales_p2b)
todas_placas_recortadas_color = []
todas_placas_recortadas_gris = []
placas_detectadas = 0
placas_no_detectadas = 0
for idx in range(num_placas):
print(f"\n{'─'*70}")
print(f"📋 IMAGEN {idx + 1}/{num_placas}")
print(f"{'─'*70}")
img_original = imagenes_originales_p2b[idx]
img_final = todas_dilataciones_binarias[idx] # ✅ CORRECCIÓN AQUÍ
# Encontrar contornos
contornos, _ = cv2.findContours(img_final, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
print(f" Total contornos: {len(contornos)}")
candidatos = []
for cnt in contornos:
area = cv2.contourArea(cnt)
if area < AREA_MIN:
continue
x, y, w, h = cv2.boundingRect(cnt)
if h == 0:
continue
ar = w / float(h)
if not (AR_MIN <= ar <= AR_MAX):
continue
solidez = area / float(w * h)
if solidez < SOLIDEZ_MIN:
continue
# Calcular extent y score
rect_area = w * h
extent = area / rect_area
score = area * (1.0 / (abs(ar - 3.5) + 0.5)) * solidez * extent
candidatos.append({
'bbox': (x, y, w, h),
'area': area,
'ar': ar,
'solidez': solidez,
'extent': extent,
'score': score
})
candidatos.sort(key=lambda c: c['score'], reverse=True)
print(f" Candidatos válidos: {len(candidatos)}")
# Extraer mejor placa si existe
if len(candidatos) > 0:
mejor = candidatos[0]
x, y, w, h = mejor['bbox']
print(f" ✓ Placa detectada:")
print(f" - Área: {mejor['area']:.0f}")
print(f" - AR: {mejor['ar']:.2f}")
print(f" - Solidez: {mejor['solidez']:.2f}")
# Extraer con margen
margen = 10
x1 = max(0, x - margen)
y1 = max(0, y - margen)
x2 = min(img_original.shape[1], x + w + margen)
y2 = min(img_original.shape[0], y + h + margen)
placa_color = img_original[y1:y2, x1:x2]
placa_gris = cv2.cvtColor(placa_color, cv2.COLOR_BGR2GRAY)
todas_placas_recortadas_color.append(placa_color)
todas_placas_recortadas_gris.append(placa_gris)
placas_detectadas += 1
# Visualizar esta detección
fig, axes = plt.subplots(1, 3, figsize=(20, 6))
fig.suptitle(f"Placa {idx + 1} - Detección Exitosa",
fontsize=16, fontweight='bold', color='green')
# Original
axes[0].imshow(cv2.cvtColor(img_original, cv2.COLOR_BGR2RGB))
axes[0].set_title('Imagen Original', fontsize=14, fontweight='bold')
axes[0].axis('off')
# Máscara morfológica
axes[1].imshow(img_final, cmap='gray')
axes[1].set_title('Máscara Final', fontsize=14, fontweight='bold')
axes[1].axis('off')
# Placa extraída
axes[2].imshow(cv2.cvtColor(placa_color, cv2.COLOR_BGR2RGB))
axes[2].set_title(f"Placa Extraída (AR: {mejor['ar']:.2f})",
fontsize=14, fontweight='bold', color='green')
axes[2].axis('off')
plt.tight_layout()
plt.show()
else:
print(f" ❌ No se detectó placa válida")
todas_placas_recortadas_color.append(None)
todas_placas_recortadas_gris.append(None)
placas_no_detectadas += 1
# RESUMEN FINAL
print(f"\n{'='*70}")
print(f"📊 RESUMEN FINAL")
print(f"{'='*70}")
print(f"✅ Placas detectadas: {placas_detectadas}/{num_placas}")
print(f"❌ Sin detección: {placas_no_detectadas}/{num_placas}")
print(f"{'='*70}\n")
print("✅ Variables guardadas:")
print(" - todas_placas_recortadas_color")
print(" - todas_placas_recortadas_gris")
print("\n✅ CELDA 6 completada - Listo para CELDA 7 (Rotación)")
======================================================================
🔍 DETECCIÓN Y EXTRACCIÓN DE PLACAS
======================================================================
──────────────────────────────────────────────────────────────────────
📋 IMAGEN 1/19
──────────────────────────────────────────────────────────────────────
Total contornos: 5
Candidatos válidos: 4
✓ Placa detectada:
- Área: 342135
- AR: 1.63
- Solidez: 0.87
──────────────────────────────────────────────────────────────────────
📋 IMAGEN 2/19
──────────────────────────────────────────────────────────────────────
Total contornos: 8
Candidatos válidos: 1
✓ Placa detectada:
- Área: 30228
- AR: 2.37
- Solidez: 0.52
──────────────────────────────────────────────────────────────────────
📋 IMAGEN 3/19
──────────────────────────────────────────────────────────────────────
Total contornos: 8
Candidatos válidos: 2
✓ Placa detectada:
- Área: 21862
- AR: 3.20
- Solidez: 0.74
──────────────────────────────────────────────────────────────────────
📋 IMAGEN 4/19
──────────────────────────────────────────────────────────────────────
Total contornos: 4
Candidatos válidos: 0
❌ No se detectó placa válida
──────────────────────────────────────────────────────────────────────
📋 IMAGEN 5/19
──────────────────────────────────────────────────────────────────────
Total contornos: 3
Candidatos válidos: 2
✓ Placa detectada:
- Área: 339424
- AR: 1.73
- Solidez: 0.92
──────────────────────────────────────────────────────────────────────
📋 IMAGEN 6/19
──────────────────────────────────────────────────────────────────────
Total contornos: 3
Candidatos válidos: 1
✓ Placa detectada:
- Área: 15934
- AR: 1.92
- Solidez: 0.96
──────────────────────────────────────────────────────────────────────
📋 IMAGEN 7/19
──────────────────────────────────────────────────────────────────────
Total contornos: 3
Candidatos válidos: 0
❌ No se detectó placa válida
──────────────────────────────────────────────────────────────────────
📋 IMAGEN 8/19
──────────────────────────────────────────────────────────────────────
Total contornos: 16
Candidatos válidos: 1
✓ Placa detectada:
- Área: 25280
- AR: 1.84
- Solidez: 0.51
──────────────────────────────────────────────────────────────────────
📋 IMAGEN 9/19
──────────────────────────────────────────────────────────────────────
Total contornos: 3
Candidatos válidos: 2
✓ Placa detectada:
- Área: 89038
- AR: 3.53
- Solidez: 0.67
──────────────────────────────────────────────────────────────────────
📋 IMAGEN 10/19
──────────────────────────────────────────────────────────────────────
Total contornos: 13
Candidatos válidos: 2
✓ Placa detectada:
- Área: 17972
- AR: 3.29
- Solidez: 0.72
──────────────────────────────────────────────────────────────────────
📋 IMAGEN 11/19
──────────────────────────────────────────────────────────────────────
Total contornos: 5
Candidatos válidos: 2
✓ Placa detectada:
- Área: 65216
- AR: 2.81
- Solidez: 0.52
──────────────────────────────────────────────────────────────────────
📋 IMAGEN 12/19
──────────────────────────────────────────────────────────────────────
Total contornos: 6
Candidatos válidos: 2
✓ Placa detectada:
- Área: 103242
- AR: 4.42
- Solidez: 0.71
──────────────────────────────────────────────────────────────────────
📋 IMAGEN 13/19
──────────────────────────────────────────────────────────────────────
Total contornos: 11
Candidatos válidos: 3
✓ Placa detectada:
- Área: 39642
- AR: 3.26
- Solidez: 0.62
──────────────────────────────────────────────────────────────────────
📋 IMAGEN 14/19
──────────────────────────────────────────────────────────────────────
Total contornos: 8
Candidatos válidos: 2
✓ Placa detectada:
- Área: 168199
- AR: 2.38
- Solidez: 0.86
──────────────────────────────────────────────────────────────────────
📋 IMAGEN 15/19
──────────────────────────────────────────────────────────────────────
Total contornos: 3
Candidatos válidos: 0
❌ No se detectó placa válida
──────────────────────────────────────────────────────────────────────
📋 IMAGEN 16/19
──────────────────────────────────────────────────────────────────────
Total contornos: 9
Candidatos válidos: 1
✓ Placa detectada:
- Área: 73124
- AR: 2.81
- Solidez: 0.60
──────────────────────────────────────────────────────────────────────
📋 IMAGEN 17/19
──────────────────────────────────────────────────────────────────────
Total contornos: 11
Candidatos válidos: 3
✓ Placa detectada:
- Área: 39642
- AR: 3.26
- Solidez: 0.62
──────────────────────────────────────────────────────────────────────
📋 IMAGEN 18/19
──────────────────────────────────────────────────────────────────────
Total contornos: 16
Candidatos válidos: 1
✓ Placa detectada:
- Área: 25280
- AR: 1.84
- Solidez: 0.51
──────────────────────────────────────────────────────────────────────
📋 IMAGEN 19/19
──────────────────────────────────────────────────────────────────────
Total contornos: 7
Candidatos válidos: 3
✓ Placa detectada:
- Área: 242986
- AR: 1.75
- Solidez: 0.67
====================================================================== 📊 RESUMEN FINAL ====================================================================== ✅ Placas detectadas: 16/19 ❌ Sin detección: 3/19 ====================================================================== ✅ Variables guardadas: - todas_placas_recortadas_color - todas_placas_recortadas_gris ✅ CELDA 6 completada - Listo para CELDA 7 (Rotación)
Conclusiones¶
Resultados del Mini Proyecto¶
Este apéndice demostró exitosamente la aplicación secuencial de operaciones morfológicas en un problema real de detección de objetos. El pipeline implementado logró detectar y extraer placas vehiculares utilizando las seis operaciones morfológicas fundamentales estudiadas en el curso principal.
Desafíos Técnicos Encontrados¶
1. Ajuste de Parámetros de Kernels¶
El proceso de calibración de elementos estructurantes fue iterativo y requirió experimentación extensa:
Top-Hat kernel (35×18): Se probaron tamaños desde 15×10 hasta 50×25. Kernels pequeños no capturaban la región completa de la placa, mientras que kernels muy grandes incluían demasiado ruido del fondo.
Cierre morfológico (30×5): Este kernel rectangular fue crítico para conectar caracteres fragmentados. La proporción ancho:alto tuvo que ajustarse específicamente para placas horizontales. Valores como 20×3 dejaban huecos, mientras que 40×10 conectaban caracteres con ruido adyacente.
Dilatación final (20×8): El balance entre expandir suficiente la región sin incluir componentes no deseados requirió múltiples iteraciones.
2. Variabilidad en Dimensiones de Imágenes¶
Las imágenes del dataset presentaron diferentes resoluciones y aspect ratios:
Redimensionado: Se estandarizó a 800px de ancho, pero esto causó pérdida de detalle en imágenes de alta resolución originales.
Impacto en kernels: Los tamaños de kernel óptimos variaron según la escala de la imagen. Un kernel 30×5 efectivo en una imagen de 800px resultó insuficiente en imágenes más grandes.
3. Casos de Fallo¶
Placas no detectadas:
- Imágenes con iluminación extremadamente baja donde el contraste entre placa y vehículo era mínimo
- Placas parcialmente ocluidas o con ángulos de visión muy oblicuos
- Fondos con regiones claras similares al tono de la placa (falsos positivos competitivos)
Detecciones incorrectas:
- Regiones rectangulares del vehículo (parrillas, molduras) que cumplían criterios geométricos
- Reflejos o elementos metálicos con alto contraste
4. Limitaciones del Pipeline Morfológico¶
- Sensibilidad a iluminación: Las operaciones de umbralización (Otsu) fallaron en escenas de alto rango dinámico
- Pérdida de información: El Top-Hat puede suprimir placas oscuras sobre fondos claros
- Conectividad excesiva: El cierre morfológico ocasionalmente conectó la placa con componentes cercanos del vehículo
Aprendizajes Clave¶
No existe un kernel universal: Los parámetros morfológicos son altamente dependientes del contexto y escala de la aplicación.
Combinación estratégica: La secuencia Opening → Top-Hat → Gradient → Closing resultó más efectiva que operaciones individuales. Cada paso prepara la imagen para el siguiente.
Trade-offs inevitables: Mayor conectividad (cierre agresivo) mejora la detección de placas fragmentadas pero aumenta falsos positivos. El balance depende de la aplicación específica.
Preprocesamiento crítico: El filtro bilateral y la ecualización CLAHE fueron tan importantes como las operaciones morfológicas en sí.
Trabajo Futuro y Mejoras Propuestas¶
Mejoras Técnicas¶
Kernels adaptativos:
- Implementar selección automática de tamaño de kernel basado en la resolución de entrada
- Utilizar múltiples kernels en paralelo y fusionar resultados
Umbralización adaptativa:
- Reemplazar Otsu global con umbralización adaptativa local
- Implementar binarización robusta a variaciones de iluminación (algoritmo de Sauvola o Wolf)
Pipeline híbrido:
- Combinar morfología clásica con detección basada en gradientes (Canny, Sobel)
- Implementar detector de regiones MSER (Maximally Stable Extremal Regions) como complemento
Post-procesamiento geométrico:
- Añadir corrección de perspectiva para placas oblicuas
- Implementar verificación de patrones (texto horizontal esperado)
Extensiones del Proyecto¶
Morfología en escala de grises:
- Explorar operaciones morfológicas directamente en imágenes grises sin binarización
- Implementar reconstrucción morfológica para mejor extracción de regiones
OCR integrado:
- Añadir reconocimiento de caracteres usando Tesseract
- Validar detecciones mediante patrones alfanuméricos esperados
Deep Learning híbrido:
- Utilizar morfología como preprocesamiento para redes neuronales (YOLO, Faster R-CNN)
- Comparar rendimiento: morfología clásica vs. detección por CNN
Optimización de rendimiento:
- Implementación GPU con CUDA para operaciones morfológicas en tiempo real
- Pipeline de procesamiento paralelo para múltiples imágenes
Dataset expandido:
- Incluir placas de diferentes países y formatos
- Aumentación de datos con variaciones de iluminación, ruido y oclusión controlada
Reflexión Final¶
Este mini proyecto validó la efectividad de las operaciones morfológicas en aplicaciones reales de visión computacional. Aunque técnicas modernas de deep learning pueden superar el rendimiento en detección, la morfología matemática ofrece ventajas importantes:
- Interpretabilidad: Cada operación tiene un significado geométrico claro
- Eficiencia computacional: No requiere entrenamiento ni GPUs potentes
- Control explícito: Los parámetros son ajustables y predecibles
- Base sólida: Comprensión fundamental aplicable a cualquier framework moderno
La experiencia de calibración manual de kernels y umbrales proporcionó intuición valiosa sobre el comportamiento de operadores morfológicos que trasciende cualquier implementación específica.
Referencias¶
Gonzalez, R. C., & Woods, R. E. (2018). Digital image processing (4th ed.). Pearson.
Haralick, R. M., Sternberg, S. R., & Zhuang, X. (1987). Image analysis using mathematical morphology. IEEE Transactions on Pattern Analysis and Machine Intelligence, 9(4), 532-550. https://doi.org/10.1109/TPAMI.1987.4767941
OpenCV. (2024). Morphological transformations. OpenCV Documentation. https://docs.opencv.org/4.x/d9/d61/tutorial_py_morphological_ops.html
Serra, J. (1982). Image analysis and mathematical morphology. Academic Press.
Soille, P. (2003). Morphological image analysis: Principles and applications (2nd ed.). Springer-Verlag. https://doi.org/10.1007/978-3-662-05088-0
Sonka, M., Hlavac, V., & Boyle, R. (2014). Image processing, analysis, and machine vision (4th ed.). Cengage Learning.
Vincent, L. (1993). Morphological grayscale reconstruction in image analysis: Applications and efficient algorithms. IEEE Transactions on Image Processing, 2(2), 176-201. https://doi.org/10.1109/83.217222
Tecnológico de Monterrey
Maestría en Inteligencia Artificial Aplicada
Visión Computacional para Imágenes y Video
Team 13 - Octubre 2025